LINUX.ORG.RU

Если вам не хватало UB в C, то вам принесли ещё

 ,


1

3

Привет, мои дорогие любители сишки!

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

— zero-sized reallocations with realloc are undefined behavior;

То есть вот это валидный код:

void *ptr = malloc(0);
free(ptr);

А вот это – UB:

void *ptr = malloc(4096);
ptr = realloc(ptr, 0); <-- хаха UB

И это несмотря на то, что в манах уже давно написано следующее:

If size is equal to zero, and ptr is not NULL, then the call is equivalent to free(ptr)

Изменение вносится задним числом, наделяя кучу корректного (согласно документации glibc) кода способностью полностью изменить логику работы программы. Ведь это то, чего нам так не хватало!

В тред призываются известные эксперты по C: @Stanson и @alex1101, возможно они смогут нам объяснить, зачем разработчики стандарта C постоянно пытаются отстрелить себе обе ноги самыми нелепыми способами.



Последнее исправление: cumvillain (всего исправлений: 1)

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

Поддержу. Когда я в 1993 году устроился на работу, весь парк нашей конторы состоял из ЕC-1841/ЕC-1842. Никаких виндоус не было и в помине. Потом, наверное, через год, купили что-то типа 286 для электронщика, чтобы схемы мог разводить. Ну потом уже и до нас дошёл прогресс.

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

А эффективный case на большинстве архитектур реализуется при помощи… перехода по вычислимому адресу.

Так если «большинство» клонируют друг друга…

На Эльбрусе вот так:

https://ce.mentality.rip/z/z97nav

веток case должно быть не три, а штук тридцать

Пока их меньше, чем 2^7, быстрее сделать 7 сравнений, чем готовить переход по произвольному адресу.

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

То есть «до семи сравнений» обходятся блоку предсказания ветвлений дешевле, чем один промах? В чем логика? Если есть промах, то он в любом случае промах, что так, что этак.

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

То, что Windows 3.0 еле шевелилась на 8086 не доказывает, что в 1990-ом основной парк персоналок составляли машины на базе 80486 или даже на базе 80386* (кстати говоря, нормальные конфигурации 80386 в 1990-ом стоили не одну тысячу тогдашних USD, что делало обновление парка вычислительной техники дорогой задачей).

Это всё не важно вообще, на самом деле. Важно то, что даже днищенская производительность этих коробок никак не мешала сделать чтение неинициализированных переменных вполне себе определённым поведением либо ошибкой. Ну, другим языкам не мешала в те времена.

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

никак не мешала сделать чтение неинициализированных переменных вполне себе определённым поведением либо ошибкой

але. переменные могут быть разных типов, и если вы еще придумаете значение для «неинициализированного указателя», типа не 0, а 1, то откуда вы возьмете такое значение для неинициализированного int, float, bool, struct{}, union{} и тепе?

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

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

Я как-то не очень понимаю. Вот выше приводили пример, вроде такого плана:

extern void f(int *p);

void g() {
  int i;
  f(&i);
}

Во время компиляции g() компилятор вообще не знает про то, что делает f и делает ли f чтение из p до того, как в p что-то запишет.

Как воспрепятствовать этому в run-time? Заводить рядом с i какой-то признак, что она не инициализирована? А потом проверять этот признак?

Ну так я расскажу вам как в том самом далеком прошлом при разработке вычислительных программ на Turbo Pascal-е директивами компилятора принудительно отключали проверки индексов массивов. В противном случае скорость работы была ниже плинтуса. А тогдашние оптимизирующие компиляторы не были настолько оптимизирующими, чтобы убирать заведомо положительные провеки.

Про проверку во время компиляции лично мне так же не понятно.

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

Во время компиляции g() компилятор вообще не знает про то, что делает f и делает ли f чтение из p до того, как в p что-то запишет.

Более того, если p указатель не на int, а на структуру, то она вообще может оказаться в «полуинициализированном» состоянии как до входа в f(), так и после выхода из неё.

А если это union, то вообще непонятно, что с ним делать.

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

Как воспрепятствовать этому в run-time? Заводить рядом с i какой-то признак, что она не инициализирована? А потом проверять этот признак?

Так отладочные средства типа волгринда работают. Они способны выдавать диагностики вида «чтение из неинициализированной области на стеке». Ну и скорость работы там соответствующая. Это чисто для отлова сложных случаев.

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

Там нет промаха. Там есть несколько регистров переходов, в которые можно загрузить адреса. И есть широкая команда, которая в один такт может выполнять несколько команд.

Сам переход занимает один такт. Но от загрузки числового регистра в регистр переход должно пройти не менее 9 тактов. А от загрузки метки до перехода всего 2 такта.

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

Более того, если p указатель не на int, а на структуру, то она вообще может оказаться в «полуинициализированном» состоянии как до входа в f(), так и после выхода из неё.

Из той же оперы:

void g() {
  int i[4];
  i[0] = 0;
  i[2] = 2;
  i[3] = 3;
  f(&i[1]);
}
eao197 ★★★★★
()
Ответ на: комментарий от monk

Там нет промаха.

Но от загрузки числового регистра в регистр переход должно пройти не менее 9 тактов. А от загрузки метки до перехода всего 2 такта.

Я таки не до конца посыл понял - предсказатель в этом чипе имеется, или нет?

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

Компилятор вместо предсказателя ветвлений? Это самое оригинальное, что я слышал в этом треде, пожалуй. Это как сказать «апельсин вместо удобной кровати». Ортогональные вещи.

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

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

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

А зачем, если профилировка показала, например, что в 90% случаев будет первая ветка? Даже в C++ запихнули likely/unlikely. И предсказатель ветвлений больше мешает, если поток данных периодически меняется (условно первые 5 раз первая ветка, следующие 5 вторая, следующие 5 опять первая …). Компилятором можно сделать флажок, переключающий подгрузку нужной ветки вовремя. А процессор будет работать медленнее, чем без предсказателя вообще.

Собственно, в следующей версии он, скорее всего, будет для того, чтобы заранее кэш подгружать, но суть от этого не изменится. Всё равно возможность уйти за 3 такта (а для первого условия за 1 такт) намного лучше, чем переход за 9.

monk ★★★★★
()

Кстати, академический вопрос знатокам.

У меня тут мой самописный компилятор на очередной итерации работы над логикой кодогенератора произвёл на свет следующий фрагмент:

        xor     EAX, EAX
        mov     dword [@@DATA+776656], EAX
        inc     EAX
        mov     dword [@@DATA+49268], EAX
        mov     dword [@@DATA+49272], EAX
        dec     EAX
        mov     dword [@@DATA+49264], EAX
        inc     EAX
        mov     dword [@@DATA+776668], EAX

Вопрос такой, насколько это НЕЭФФЕКТИВНЫЙ код для современной суперскалярной архитектуры? Такой как Intel Core и подобные.

Вопрос касается зависимостей по данным, возникающих на операциях inc и dec.

Я эту ситуацию предполагаю так:

  • Для «тупого» последовательно работающего процессора такой код не является проблемой, поскольку «забегать вперёд» и отправлять на конвеер по несколько команд он всё равно не умеет.
  • Для современного суперскалярного CPU с большим регистровым файлом и умной логикой переименования регистров, этот код скорее всего тоже не представляет сложности.
  • Данный код, скорее всего, будет неэффективным на упрощенных суперскалярных CPU (возможно, уровня Pentium 2 или Pentium III), которые упрутся в ожидание готовности АЛУ, в недостаточный размер регистрового файла для переименования регистров, или в неспособность достаточно разумно разложить этот код на параллельные микрооперации.
wandrien ★★
()
Последнее исправление: wandrien (всего исправлений: 3)
Ответ на: комментарий от eao197

Я как-то не очень понимаю. Вот выше приводили пример, вроде такого плана:

Всё просто: считаем этот код ошибочным и шлём говнокодеров нахрен. Я выше уже написал, что можно и нужно выдавать ошибку на любые операции с (потенциально) неинициализированными переменными кроме присваивания.

extern void f(int *p);

void g() {
  int i = 0;
  f(&i);
}

Смари, вот тебе правильный код. Совсем несложно же, правда?

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

тоже объявляем неправильным?

Вообще не вижу проблемы. Но сейчас мне тут сишники накидают, конечно же, что устранение целого класса ошибок ломает их говнокод.

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

Занулять 16 килобайт памяти на каждом входе в функцию. Как мило.

А зачем? Если у тебя компилятор видит тело другой функции и может, например, её заинлайнить, то изначальная инициализация будет выкинута. А если нет, то ты не можешь расчитывать что там не будет говна, т.е. чтения из неинициализированной памяти.

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

А зачем?

Ты меня спрашиваешь? Это ТЫ предложил.

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

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

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

Так что этот трейдофф из начала 90-х наверное выглядел размно. Но современный пропосал выглядит БОЛЕЕ РАЗУМНО. Надеюсь, его примут в той или иной форме.

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

Ну вот пропосал в крестах разумный, чем тебе не нравится-то.

Добавить по сути конструктор в каждому типу, который будет инициализировать всё? Это ровно то что я написал.

Может и в Си притащат лет через 10, они ж там тормоза.

Я надеюсь, к тому времени сишники сдохнут от старости. Они уже потихоньку это делают.

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

Добавить по сути конструктор в каждому типу, который будет инициализировать всё? Это ровно то что я написал.

Нет, там не в этом суть.

Там в том суть, что:

  • В conforming режиме реализация должна инициализировать объекты на стеке некоторым значением, если они не имеют конструктора по умолчанию. Такие объекты при этом всё еще считаются «неинициализированными».
  • Ей следует, но не строго обязательно, уметь обнаруживать случаи «use before init» и репортить по ним диагностики при сборке.
  • Может переключаться в non-conforming режим, в котором имеет старое поведение без инициализации.

Да, это полумеры. Но это грамотные полумеры с учётом наличия огромных кодовых баз.

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

В conforming режиме реализация должна инициализировать объекты на стеке некоторым значением, если они не имеют конструктора по умолчанию. Такие объекты при этом всё еще считаются «неинициализированными».

В принципе, небольшим объектам можно прямо сейчас без проблем воткнуть дефолтные значения. Как ты выше написал, единственная проблема может вылезти с большими массивами. Можно кидать на них warning, например.

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

Вопрос такой, насколько это НЕЭФФЕКТИВНЫЙ код для современной суперскалярной архитектуры?

Лучше так не делать, конечно. Насколько оно вылезет - надо мерять, и с учётом всего кода around.

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

то откуда вы возьмете такое значение для неинициализированного int, float, bool, struct{}, union{} и тепе?

Просто запретить объявлять неинициализированные переменные, если это нужно (а это гораздо реже чем нужны инициализированные) то также явно это объявлять, например как выше писали для D через «= void».

anonymous
()