LINUX.ORG.RU

Авторы Си — наркоманы?

 , , ,


1

5

Столкнулся с интересным багом. После того как разобрался, что же именно происходит, меня постигло крайнее изумление! Оказывается, в языке Си тип числовой константы зависит от формата записи.

Дистиллированный пример кода, который это демонстрирует:

#include <stdbool.h>
#include <stdio.h>

#define IS_HEX(x) \
    _Generic((x), \
        unsigned int: true, \
        long: false \
    )

#define X 0x80000001
#define I 2147483649

int main(void) {
    if(X == I)
        puts("X == I");

    if(!IS_HEX(I))
        puts("I is not hexadecimal");

    if(IS_HEX(X))
        puts("X is hexadecimal");

    return 0;
}

Все три сообщения будут выведены на экран.

Зачем это сделано? Кому от этого легче? Какие оптимизации это позволяет проворачивать, кроме оптимизации отстрела ног программистам? Непонятно! В общем, стремлюсь поделиться своим негодованием здесь и предостеречь будущие поколения от наступления на эти грабли.



Последнее исправление: dataman (всего исправлений: 1)
Ответ на: комментарий от monk

Вряд ли. int скорее всего останется где был, т.к. иначе 32-битное целое уже не назвать. long, long long, long long long итд для следующих битностей. Но вот учитывать что int может быть и 16 и 32 - надо. А так же то, что long может быть и 32 и 64, а long long - и 64 и 128 (в будущем).

firkax ★★★★★
()
Ответ на: комментарий от AnonymUser

C(89) как раз довольно четко стандартизирован.

Осмелюсь добавить, математически или логически выверен, с ясной мнемоникой.

Чо?

C создан в 70-х годах, стандартизирован в 89.

Си из 70х и C89 – фактически разные языки.

yorshka
() автор топика
Ответ на: комментарий от firkax

Вряд ли. int скорее всего останется где был, т.к. иначе 32-битное целое уже не назвать.

Напротив. Логично int называть оптимальное для вычислений число. Скорее всего, размером с регистр, но тут от системы команд зависит. А дальше как обычно - комбинациями short / long.

128 (в будущем).

Не факт, что 128-битные машины в обозримом будущем появятся. Какую величину там хранить? Вот 128-битные расширения, скажем, для дробных чисел - возможно.

COKPOWEHEU
()
Ответ на: комментарий от COKPOWEHEU

Предлагаешь short short int для 16 бит, short int для 32 и int для 64? Мне кажется от такого очень много всего поплывёт, разрядность short int не менялась ни разу за всю историю Си и к ней куча всего прибито. Да и к 32-битному int-у много где прибито уже, хотя он и бывает другой. Так что остаётся всё-таки добавлять long-и для новых разрядностей.

А 128-битный int уже есть, если что - начиная с gcc 4.6 на 64-битных платформах есть __int128. До 4.6 был __int128_t но он был с багами, так что можно не учитывать. Ну и на 32-битных платформах вполне есть 64-битный long long уже давно.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от COKPOWEHEU

Достаточно частый, потому что может работать быстро(уб-оптимизация) и позволяет безболезненную арифметику а-б, где б>а

BruteForce ★★★★
()
Ответ на: комментарий от firkax

Предлагаешь short short int для 16 бит, short int для 32 и int для 64

А может просто… Ну, например… Я даже не знаю… взять уже типы из stdint.h с известной размерностью и перестать трахать мозги себе и окружающим?

Нет, «вагон проблем» вылезает из-за того, что кто-то решил доверять графоманским документам в качестве истины. Задача Си - привести более-менее к универсальному виду прогание под разные платформы, при этом по возможности не потеряв индивидуальные преимущества и просто особенности каждой. Подчёркнутое - важно. Полная унификация приведёт к потере особенностей платформ, а это как раз то, чего допускать нельзя.

Расскажи мне, какие индивидуальное преимущество эксплуатируется в следующем коде?

#include <stdbool.h>
#include <stdio.h>

int main(void)
{
  bool b;
  if(b) puts("true");
  if(!b) puts("false";)
  return 0;
}

Угадаешь, какой код генерит Clang для этого?

Примерно вот такой:

main:
yorshka
() автор топика
Ответ на: комментарий от firkax

Зачем добавлять проверку на переполнение?

Там чтение массива. int idx = i * (run_config->domain_n + 2) + j; Для механизма предподкачки массива надо иметь гарантию, что это число растёт пока работает цикл. Если i или j имеют беззнаковый тип и могут переполниться, то механизм использовать нельзя, чтобы избежать аварийного завершения.

monk ★★★★★
()
Ответ на: комментарий от monk

А эта предподкачка нужна вообще на x86-64? Как бы при линейном (с фиксированным stride) проходе процессор сам врубает prefetch.

shdown ★★
()
Последнее исправление: shdown (всего исправлений: 1)
Ответ на: комментарий от algo

Понятия не имею. Упустил, что нить обсуждения про эльбрусы.

shdown ★★
()
Ответ на: комментарий от firkax

Предлагаешь short short int для 16 бит, short int для 32 и int для 64?

Почему бы и нет. long long int ведь существует.

разрядность short int не менялась ни разу за всю историю Си и к ней куча всего прибито

Так может, пора «отбить» и переписать наконец в соответствии со стандартом? Ну там uint16_t например.

А 128-битный int уже есть

В risc-v тоже предусмотрен 128-битный стандарт. Не софтовая эмуляция, а именно разрядность ядра. Не уверен, что он до конца допиленный, но все же. Но ни одному производителю это не интересно. Вопрос-то именно в этом: так ли много задач, где нужны настолько большие целые.

COKPOWEHEU
()
Ответ на: комментарий от firkax

А 128-битный int уже есть, если что - начиная с gcc 4.6 на 64-битных платформах есть __int128. До 4.6 был __int128_t но он был с багами, так что можно не учитывать. Ну и на 32-битных платформах вполне есть 64-битный long long уже давно.

Кстати, он есть и 256-битный и даже 65535-битный.

#include <limits.h>
#include <stdio.h>

typedef unsigned _BitInt(8388608) enormous;

int main() {
  enormous i = -1;
  printf("%d\n", BITINT_MAXWIDTH);
  return 0;
}

Скомпилируй с -std=c23 и удивись.

$ gcc bitint.c -std=c23
$ ./a.out 
65535
$ clang bitint.c -std=c23
$ ./a.out 
8388608

ВОСЬМИМЕГАБАЙТОВОГО ИНТА ХВАТИТ ВСЕМ!

yorshka
() автор топика
Ответ на: комментарий от firkax

О! Вот теперь понятно, откуда и зачем в итоге в C ассемблерные вставки.

AnonymUser
()
Ответ на: комментарий от yorshka

Да-да. 6 лет люди (аж целая комиссия, подозреваю, что научная) бились над стандартизацией C и получили (прекрасно выверенный) C89. Который используется до сих пор, включая научную сферу, что во многом и определяет его живучесть. А наука и научная сфера, к слову сказать, исторична. Это я к тому, что уже ничего не поправить, история C уже сложилась. И большая часть полезного написана на нем. C89 с нами навсегда, минимум, как учебный язык, как псевдокод и т.д.

Единственное, подозреваю, что у версий C есть преемственность. И думаю, она будет сохранятся. Иначе всё напрасно. Т.е. через 100 лет, если живы будут люди, можно будет скомпилировать код C89.

P.S. Я есть. =) Я тот самый человек, который писал на C89, после того, как мне надоел PHP (на котором я ранее писал прототипы). И писал до недавнего времени. Последнюю программу написал 1.5 года назад. Сейчас усомнился и даже попросил нейронную сеть определить стандарт своего кода - все на С89. Я прям доволен.

P.P.S. Я свои программы проверял на: Win32 и Win64, на Linux и Android (armv7), даже на ek2 (Эльбрус, был когда-то демо-доступ к общедоступному стенду). Некоторые из них, совсем стандартные, работают без изменений.

AnonymUser
()
Последнее исправление: AnonymUser (всего исправлений: 1)
Ответ на: комментарий от yorshka

А может просто… Ну, например… Я даже не знаю… взять уже типы из stdint.h с известной размерностью и перестать трахать мозги себе и окружающим?

Я типы с известной размерностью использую, но беру не из stdint.h а из fcl/types.h. Однако речь была не про них, а про возможные изменения разрядности простого int и возможные проблемы (или их отсутствие) совместимости кода, его использующего. Так что твой комментарий не в тему.

Расскажи мне, какие индивидуальное преимущество эксплуатируется в следующем коде?

Никакое, это баг шланга. Речь была опять не про это, а про то что на разных платформах разное поведение и не надо этого стесняться. И UB придумывать в этих местах тоже не надо, как это некие графоманы делают.

firkax ★★★★★
()
Ответ на: комментарий от COKPOWEHEU

Так может, пора «отбить» и переписать наконец в соответствии со стандартом? Ну там uint16_t например.

Нет, лучше отобьём руки тем кто хочет портить старый код на ровном месте.

firkax ★★★★★
()
Ответ на: комментарий от monk

Там слишком длинная статья, я её не осилил. Посмотрел упомянутый цикл, напишу его тут чтобы избавить остальных интресующихся от мучительных поисков

for (int i = start_i; i <= stop_i; ++i) {
    for (int j = start_j; j <= stop_j; ++j) {
        int idx = i * (run_config->domain_n + 2) + j;
        double wij  = src[idx];
        double wipj = src[(i + 1) * (run_config->domain_n + 2) + j];
        double wimj = src[(i - 1) * (run_config->domain_n + 2) + j];
        double wijp = src[i       * (run_config->domain_n + 2) + j + 1];
        double wijm = src[i       * (run_config->domain_n + 2) + j - 1];
        double x = (run_config->start_i + i - 1) * run_config->h1;
        double y = (run_config->start_j + j - 1) * run_config->h2;
        double laplacian = (wipj + wimj - 2 * wij) * run_config->sqinv_h1 + (wijp + wijm - 2 * wij) * run_config->sqinv_h2;
        dst[idx] = q(x, y) * wij - laplacian - alpha * F(x, y);
    }
}

И немного пооффтоплю (кажется, это не связано с упомянутыми оптимизациями int-long-unsined int), хотя с другой стороны не оффтоп т.к. тема про Си. Так вот, код на мой взгляд совершенно сомнительный, я бы его писал так:

mul = run_config->domain_n + 2;
for (int i = start_i; i <= stop_i; ++i) {
    pos = src + i*mul + start_j;
    for (int j=start_j; j <= stop_j; ++j,++pos) {
        double wij  = pos[0];
        double wipj = pos[mul];
        double wimj = pos[-mul];
        double wijp = pos[+1];
        double wijm = pos[-1];
        double x = (run_config->start_i + i - 1) * run_config->h1;
        double y = (run_config->start_j + j - 1) * run_config->h2;
        double laplacian = (wipj + wimj - 2 * wij) * run_config->sqinv_h1 + (wijp + wijm - 2 * wij) * run_config->sqinv_h2;
        dst[idx] = q(x, y) * wij - laplacian - alpha * F(x, y);
    }
}

И выглядит намного опрятнее, и, подозреваю, компилятору будет проще заметить что строка массива (индексы по j) непрерывна безо всякого signed-ub-подкостыливания.

firkax ★★★★★
()
Ответ на: комментарий от AnonymUser

Да-да. 6 лет люди (аж целая комиссия, подозреваю, что научная) бились над стандартизацией C и получили (прекрасно выверенный) C89.

Научного там ничего не было. Так, сборище обрыганов. Почитай истории Реймонда (который ESR), он наблюдал за этим. Над собственно языком прошлись достаточно быстро, всего за год-два. А потом ещё 4 года срались по переписке о содержимом стандартной библиотеки.

Видел как сейчас Wayland разрабатывают, где на каждый чих собирается срач на три года? Вот сишеньку так же делали. Это просто плод больного аутизма, ничего более.

yorshka
() автор топика
Ответ на: комментарий от firkax

Никакое, это баг шланга.

В gcc код просто печатает false. Это тоже баг? Почему все компиляторы такие забагованные-то? Сишники не могут нормальный компилятор сделать?

yorshka
() автор топика
Ответ на: комментарий от yorshka

А что плохого печатать false? Твой bool размещён в виртуальном регистре, рандомно оказывающимся false на старте. Или ты хотел чтобы он генератор случайных чисел туда встроил? Или локальная переменная должна обязательно обращаться к оперативной памяти?

firkax ★★★★★
()
Ответ на: комментарий от yorshka

Не буду от себя больше писать. https://habr.com/ru/companies/vdsina/articles/532416/ 4 комментарий.

Цитирую: «Инструмент вытащили далеко за области его применимости, и обнаружили — внезапно! — что он там плохо работает. И решили его заменить. Ну, да, писать либу размера и функциональности как OpenSSL на С — так себе начинание. Насчёт ядра Linux, например, уже нельзя сказать так уверенно. А что-то, что работает без ОС, «оживить» с помощью С — самое то. Но ведь это нюансы, о которых мало кто думает. У меня многие знакомые программисты при слове «программировать» не вспоминают embedded даже с подсказкой. Типа «на железках дрова сами заводятся, как мыши в сене».»

Пишите себе на чем хотите и оставьте в покое C. Системщики все равно будут писать на нем.

AnonymUser
()
Ответ на: комментарий от AnonymUser

habr

Извини, это помойка и статьи там аналогичные.

У меня многие знакомые программисты при слове «программировать» не вспоминают embedded даже с подсказкой

Потому что перекладывание байтиков из регистра в регистр – это не программирование. Не больше чем перекладывания JSON из сокета в Postgres. Примерно одного уровня занятие, и не случайно, что LLM-ки первым делом теснят как раз этих ребят.

Пишите себе на чем хотите и оставьте в покое C. Системщики все равно будут писать на нем.

Ага. С громкими матами и плюясь по сторонам, как я это делаю.

yorshka
() автор топика
Ответ на: комментарий от firkax

А что плохого печатать false? Твой bool размещён в виртуальном регистре, рандомно оказывающимся false на старте. Или ты хотел чтобы он генератор случайных чисел туда встроил? Или локальная переменная должна обязательно обращаться к оперативной памяти?

Ты не понял. GCC выкидывает проверки и заменяет всю программу на int main(void) { puts("false"); }

Внезапно, единственный из популярной тройки, который этого не делает, это MSVC. Тот честно оставляет все проверки. Наверное, только Microsoft могут сишный компилятор без багов сделать.

yorshka
() автор топика
Ответ на: комментарий от firkax

Нет, лучше отобьём руки тем кто хочет портить старый код на ровном месте.

Тоже неплохо. Глядишь, хоть в новом коде подобного делать не будете. Ну, по крайней мере пока руки не заживут.

Ты не понял. GCC выкидывает проверки и заменяет всю программу на int main(void) { puts("false"); }

А что, он не имеет права этого делать? Компилятор решил, что в неинициализированной переменной лежит ноль. Это ничему не противоречит. В отличие от clang с его нерешительной переменной, которая и не true и не false.

COKPOWEHEU
()
Ответ на: комментарий от yorshka

Потому что перекладывание байтиков из регистра в регистр – это не программирование. Не больше чем перекладывания JSON из сокета в Postgres. Примерно одного уровня занятие, и не случайно, что LLM-ки первым делом теснят как раз этих ребят.

Интересно, LLM-ки осилят написать корректно хоть что-то, что сегодня (по делу, а не for fun) написано на ассемблере? Я уж не говорю про производительность.

Ушёл проверять.

shdown ★★
()
Ответ на: комментарий от COKPOWEHEU

Ты не понял. GCC выкидывает проверки и заменяет всю программу на int main(void) { puts(«false»); }

А что, он не имеет права этого делать? Компилятор решил, что в неинициализированной переменной лежит ноль. Это ничему не противоречит. В отличие от clang с его нерешительной переменной, которая и не true и не false.

Согласно мнению @firkax, это баг.

Вообще, конечно, это UB и здесь нет гарантий на поведение кода вообще никаких, поэтому выкинуть такую программу и заменить на тупо return 0; будет корректным. Как и любое другое поведение.

yorshka
() автор топика
Ответ на: комментарий от yorshka

Согласно мнению @firkax, это баг.

Он писал, что баг - выкидывание вообще всего кода, как будто в переменной и не 0 и не любое другое число. Такое поведение действительно нелогично. А в gcc как раз ожидаемо: без явной инициализации в переменной скорее всего будет ноль. Такое значение было бы, будь переменная глобальной, такое скорее всего написал бы программист.

Есть, конечно, еще вариант максимизации ошибки - clang изо всех сил пытался намекнуть на некорректность кода. Но это вряд ли.

COKPOWEHEU
()
Ответ на: комментарий от COKPOWEHEU

Там хуже - он выкинул даже return; и этим устроил багоповедение на уровне ассемблера.

без явной инициализации в переменной скорее всего будет ноль.

Ну нет, не совсем так. В переменной может быть ноль, и поскольку нет причин выбрать другое (переменная ни разу не используется в качестве lvalue и никому снаружи, где её мог бы взять как lvalue кто-то другой, недоступна), то мапим её на абстрактный ноль для определённости.

А если бы она была в оперативной памяти, то тут скорее всего тоже был бы ноль, но только потому что это main и эту часть стека ещё скорее всего не использовали. Но в общем случае - совсем не факт.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 3)
Ответ на: комментарий от firkax

Плохо то, что это радикально меняет поведение кода и что это поведение различно для разных типов. Например, если там будет массив вместо bool, оптимизация будет совершенно другой. Или это нормально в твоей картине?

yorshka
() автор топика
Ответ на: комментарий от yorshka

Где оно меняет поведение? Он мог оставить printf(«true») - и это тоже было бы норм. Если переменная неинициализирована, в ней может быть любое значение, и компилятору никто не запрещает подставить в неё сразу какое-то удобное ему.

Только не надо это путать с «а давайте выкинем всю функцию вместе с return-ом раз нам кажется что она не может быть».

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 2)
Ответ на: комментарий от firkax

мапим её на абстрактный ноль для определённости.

Зачем устраивать определённость, если тут UB? Есть только два разумных подхода к такому коду: 1) делать вид, что UB нет, и генерить код с чтением неинициализированного куска стека с обоими условиями; 2) относиться к этому коду так же, как к int main(void) {__builtin_unreachable();}. То, что делает шланг, логично. То, что делает гцц, не имеет никакого смысла, т.к. он одновременно и распознаёт наличие UB, и генерит код из UB.

unC0Rr ★★★★★
()
Ответ на: комментарий от unC0Rr

если тут UB?

В десятый раз повторю, что UB-графоманов следует игнорировать.

Определённость это всегда хорошо.

gcc всё сделал правильно - оптимизировал код не нарушая логику его работы (и никаких «распознаваний UB» для этого не требуется). Шланг же всё сломал и сгенерил нерабочий бинарник.

1) делать вид, что UB нет, и генерить код с чтением неинициализированного куска стека с обоими условиями;

Где ты стек то нашёл? Он тут не используется.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 4)
Ответ на: комментарий от firkax

Определённость это всегда хорошо.

Конечно, хорошо. Пишите программы с определённостями.

Кстати, если бы вы писали компилятор своей мечты, как бы вы доопределили программу ниже?

int main(void)
{
  bool *b;
  if(*b) puts("true");
  if(!*b) puts("false";)
  return 0;
}

Где ты стек то нашёл?

В строке bool b;. А где вы его потеряли?

unC0Rr ★★★★★
()
Последнее исправление: unC0Rr (всего исправлений: 1)
Ответ на: комментарий от unC0Rr

Строка bool b; не означает автоматически использование стека. Переменная вынуждена размещаться в оперативной памяти, если используется её адрес (тут есть детали), либо если для неё не хватило места в других местах. Если же ни первого ни второго не случилось - размещаться она может где угодно, в основном - в регистрах. В данном коде никаких причин сувать её в стек нет, и даже более того - её вообще незачем хранить в физическом хранилище, так как в неё ни разу не записываются никакие данные, это константа. Касательно значения данной константы со стороны программиста отсутствуют какие бы то ни было пожелания, значит она может быть любой на усмотрение компилятора. Получение её значения черед чтение произвольных байт стека/памяти допустимо, так же как и получение её значения методом его хардкода во время компиляции.

Кстати, если бы вы писали компилятор своей мечты, как бы вы доопределили программу ниже?

Вопрос некорректен. «Доопределяет» компилятор а не автор компилятора. Посмотреть можно когда компилятор уже написан, заранее планировать работу такой проги незачем. Но в данном случае, как и в прошлой проге, будут допустимы два варианта:

1) наивный (который ты представлял первым делом): мапим переменную на стек или в регистр, читаем из него неинициализированный указатель и затем разыменовываем этот неинициализированный адрес в виде bool (скорее всего будет сегфолт в итоге),

2) небольшая оптимизация: хардкодим некое значение (скорее всего NULL т.к. другие смысла делать меньше, впрочем тоже можно), и затем читаем bool с нулевого адреса (скорее всего тоже будет сегфолт).

firkax ★★★★★
()

Авторы Си — наркоманы?

Да. А ещё они дельфины. Дельфины-наркоманы.

sbu_shpigun ★★
()
Ответ на: комментарий от firkax

Ну нет, не совсем так. В переменной может быть ноль,

Я сказал «скорее всего». Не «совершенно точно». Компилятор «на основании своего опыта» решил, что наиболее вероятное значение - ноль. То есть такое же, как у глобальной переменной. Учитывая UB, он имеет право на этом и остановиться.

(переменная ни разу не используется в качестве lvalue и никому снаружи, где её мог бы взять как lvalue кто-то другой, недоступна), то мапим её на абстрактный ноль для определённости.

Вот тут нет. Используется ли она еще где-то - абсолютно безразлично. Просто пока она не инициализирована, ее использование это UB. Компилятор может предположить, что там ноль, компилятор может счесть, что там 1, компилятор может счесть, что там 100500, компилятор может счесть, что там rand(). Компилятор даже может счесть, что у программиста проблемы с головой и отформатировать винт.

Я говорю лишь о том, что поведение gcc максимально логичное. Это то, чего может ожидать программист. В отличие от clang, где компилятор счел значение и не нулевым, и не ненулевым.

Как в g++, когда он для вот такого кода напрочь выкидывал return:

#include <iostream>

int func(){
  std::cout << "func\n";
}

int main(){
  func();
}

и соответственно вся программа падает в сегфолт. Это как бы разрешенное поведение, но напрочь нелогичное.

То, что делает гцц, не имеет никакого смысла

Напротив. Он честно кидает ошибку (warning), но при этом генерирует корректный код. В самом деле, вдруг программист просто что-то пока не дописал. Протестирует что-то другое в другом месте и со временем исправит. На ошибку ведь ему указали? Указали.

COKPOWEHEU
()
Ответ на: комментарий от monk

В этом плане какой-то из CRAY был занятен)

char==short==int==long==long long )))

все 64 бит.

И float тоже как дабл.

вот не помню long double был 64 или 128)

Slackware_user ★★★★★
()
Ответ на: комментарий от yorshka

про stdint/cstdint я уже писал, там реально не всегда применимо если минимизировать макросы.

много чего требует int чем бы он не был.

Slackware_user ★★★★★
()
Ответ на: комментарий от firkax

Так вроде уже прописали все размерности, жаль с >= в качестве условия.

из-за такого теперь win64 и linux на amd64 имеют разный размер long

Slackware_user ★★★★★
()
Ответ на: комментарий от firkax

ну относительно недавно все же.

И компиляторы то обновляли. изначально не уверен что он там был.

Slackware_user ★★★★★
()
Ответ на: комментарий от COKPOWEHEU

Думаю в uint64_t

по факту в те годы основной язык для них был фортран, а там до сих пор unsigned только обсуждают вроде.

Slackware_user ★★★★★
()
Ответ на: комментарий от monk

Ну так данные то у программ часто известно какой размерности.

Если требуется что-то системное с данными вызывать то начинается магия. Если нет то int во все поля.

Slackware_user ★★★★★
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.