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)
Ответ на: комментарий от wandrien

UB используется для оптимизации.

Например, переполнение знакового целого UB, чтобы проверка вида i + 1 > i была всегда истинной.

Поэтому программа на языке без UB никогда не будет настолько быстрой, как программа на Си++.

Собственно почти так сделано в C#. Там есть checked и unchecked. MS тут отработали хорошо.

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

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

Вот прямо сейчас есть e2k. На нём Си (и Си++) быстрее Java/C# не в 1,5-2 раза как везде, а примерно в 10. Потому что семантика Си спроектирована переносимой, а C# уже гвоздями прибили к нынешней архитектуре.

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

В основном в том, что в Java/C# все функции сделали виртуальными (то есть заложились на то, что в процессоре есть дешёвый call rax). А в e2k вызов по функции по фиксированному адресу на порядок быстрее, чем по вычисленному.

Ну и ветвлений из-за лишних проверок в Java/C# больше.

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

UB используется для оптимизации.

Я пытаюсь поверить в то что Вы просто троллите, но 5 звезд под сообщением очень мешают этому.

Поэтому программа на языке без UB никогда не будет настолько быстрой, как программа на Си++.

Да, обогнать С++ по падению в сегфолт может разве что C.

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

Стрелялки с хорошей графикой, СУБД, математические библиотеки… практически всё, где требуется производительность, написано на Си++ и иногда (из-за наследия) на Си.

Бенчмарки https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/gpp-java.html

Да, обогнать С++ по падению в сегфолт может разве что C.

Если ошибок нет, в сегфолт не падает.

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

Нет, ub это ub.

Когда я учился в школе, там на стене рядом с одним из кабинетов была надпись: «Все п*?::ы - п?:*:ы». Твоё утверждение не менее экзистенциально и столь же бессмысленно.

В языке Си термин «undefined behaviour» относится именно к гарантиям по части генерируемого кода.

То, что информацию об ub компилятор использует, чтобы срать в код, это отдельная фишка современных систем компиляции.

Нет, это фишка языка C.

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

Уже с середины 1990-х мощность персоналок позволяла иметь инициализацию для всего без заметной потери производительности.

Да что ты несёшь-то? В паскале вот этой срани нет, но софт на паскале и делфях не то чтобы тормознее сишного был. Зачастую даже наоборот.

Ей богу, оправдания сишников просто поражают.

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

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

Осталось только понять, как быть на архитектурах без регистров.

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

Когда я учился в школе, там на стене рядом с одним из кабинетов была надпись: «Все п*?::ы - п?:*:ы». Твоё утверждение не менее экзистенциально и столь же бессмысленно.

Я знаю)) Я ел роллы в приятной компании и писал с телефона, на большее я был не способен)

В языке Си термин «undefined behaviour» относится именно к гарантиям по части генерируемого кода.

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

У тебя может в реализации языка вообще не быть фазы компиляции, а в конкретной реализации эти некорректные состояния могут быть даже обработаны предсказуемым образом. Но это личное дело именно этой реализации. Речь идёт лишь о том, что реализация НЕ ОБЯЗАНА их вообще как-либо обрабатывать.

Иди найди в спецификации хоть что-то про «генерируемый код». Спецификация вообще не оперирует таким понятием.

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

В языке Си термин «undefined behaviour» относится именно к гарантиям по части генерируемого кода.

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

У тебя может в реализации языка вообще не быть фазы компиляции, а в конкретной реализации эти некорректные состояния могут быть даже обработаны предсказуемым образом. Но это личное дело именно этой реализации. Речь идёт лишь о том, что реализация НЕ ОБЯЗАНА их вообще как-либо обрабатывать.

Иди найди в спецификации хоть что-то про «генерируемый код». Спецификация вообще не оперирует таким понятием.

Да пожалуйста, прямо в самом начале (секция 5.1.1.1):

The text of the program is kept
in units called source files, (or preprocessing files) in this International Standard. A
source file together with all the headers and source files included via the preprocessing
directive #include is known as a preprocessing translation unit. After preprocessing, a
preprocessing translation unit is called a translation unit. Previously translated translation
units may be preserved individually or in libraries. The separate translation units of a
program communicate by (for example) calls to functions whose identifiers have external
linkage, manipulation of objects whose identifiers have external linkage, or manipulation
of data files. Translation units may be separately translated and then later linked to
produce an executable program.

Стандарт явно разделяет фазы трансляции и выполнения.

Более того, если ты посмотришь на список UB, там есть пункты, явно не подразумевающие выполнения кода в принципе. Например, отсутствие переноса строки в самом конце файла:

non-empty source file does not end in a new-line character which is not immediately preceded by a backslash character or ends in a partial preprocessing token or comment

Надо подать перцам из GCC идею генерировать полное говно в этом случае. А что? Стандарт разрешает!

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

Из-за этого инварианта про i + 1 корректное написание кода со знаковой арифметикой превращается в танцы в гамаке на лыжах. От этого вреда больше, чем пользы.

Я описываю это следующим образом: абстрагироваться от деталей реализации вам дали, а конкретизироваться до них - нет.

Если язык описывает переполнение «обычной» арифметики как UB, то чисто из соображений практического применения в нем должны быть штатные средства работать с переполняемой арифметикой как с отдельной сущностью. А не делать невинный вид, будто у нас бесконечная разрядная сетка.

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

Да что ты несёшь-то?

В лучшем случае вру как очевидец. Начал осваивать Си в 1991-ом году еще на 8086 (даже не на 286, не говоря уже про 386, хотя на Западе уже и 486-е были). И вот на 8086 производительность не сказать, чтобы прям ух была.

Зато когда годах 1994-1995-ом в наших Палестинах уже массово стали применяться 486-е, а потом и Pentium-ы, вот тогда разница в производительности стала просто драматической. Помню как одной преподавательнице расчет по ее диссертации на 486DX2-80 запустили, он закончился минут за 5, тогда как она предполагала, что считать будет минут 40. Вот у нее тогда был шок, да.

Ей богу, оправдания сишников просто поражают.

А вот тут бы я попросил за базаром следить, я ну вот вообще не Си-шник.

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

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

Зато вот тебе про UB:

undefined behavior

behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements

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

Ну и где там про генерируемый код?

Генерируемый код == результат фазы трансляции.

Если же у тебя реализация будет тупо гонять вычисления по сгенерированному AST, то она будет вполне conforming.

Ну, да, и? В данном случае AST и будет кодом. Можно её ещё в S-Expr сконвертировать, чтобы лисперы кипятком ссались.

erroneous program construct or of erroneous data

Мы вот о таких случаях тут в основном говорим. Хотя я не очень понимаю, что значит «erroneous data», ну да и ладно.

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

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

Launched 1978

Дядя, ты на полтора десятка лет ошибся. Ты б ещё пожаловался, что у тебя на IBM/360 в 1995 году Doom тормозит.

я ну вот вообще не Си-шник.

Ещё какой сишник!

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

Хотя я не очень понимаю, что значит «erroneous data», ну да и ладно.

Ну вот мусор в указателе – это пример «erroneous data» как раз.

Если у тебя интерпретатор, в котором все указатели фейковые, а не «железные», то он может на этот случай вызвать abort().

А если это типичный компилятор типа gcc, который выдаёт «настоящий» машинный код для «портабельного ассемблера», то извини, вот тебе UB по морде.

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

Ну вот мусор в указателе – это пример «erroneous data» как раз.

Не уверен. У указателей в C есть такая штука как provenance, то есть все указатели должны изначально создаваться при наличии существующих корректных объектов. Т.е. если ты сделал что-то типа void *p = rand();, то это ошибочный код уже сам по себе.

Если у тебя интерпретатор, в котором все указатели фейковые, а не «железные», то он может на этот случай вызвать abort().

А если это типичный компилятор типа gcc, который выдаёт «настоящий» машинный код для «портабельного ассемблера», то извини, вот тебе UB по морде.

Вообще не вижу разницы. Почему в случае с GCC дефолтным значением неинициализированного указателя не может быть trap representation, вызывающий abort() при разыменовании? Это явно не добавит тормозов корректному коду вообще никак.

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

Речь же не только про неинициализированный указатель.

Например, ты можешь сделать p += i, ожидая, что в i содержится разумное значение. А там был мусор из-за алгоритмической ошибки в другой части кода.

В результате твой указатель вышел за границы целевого объекта (массива), а это – UB.

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

Дядя, ты на полтора десятка лет ошибся.

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

Как бы то ни было, я попрограммировал на 86-х и 286-х. Не нужно мне, юноша, рассказывать про их производительность.

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

А вы наверное думаете, что как только процессор появляется, он сразу же оказывается на рынке.

Ну, вообще да. Практически сразу. То, что в совок и постсовок везти что-то было очень гемморно и почти невозможно из-за автоматчиков на границах, это проблемы постсовка.

Как бы то ни было, я попрограммировал на 86-х и 286-х.

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

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

Например, ты можешь сделатьp += i, ожидая, что вiсодержится разумное значение. А там был мусор из-за алгоритмической ошибки в другой части кода.

В результате твой указатель вышел за границы целевого объекта (массива), а это – UB.

Да, и это проблема кода: отсутствие проверки при разыменовании указателя и/или адресной арифметике.

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

Почему в случае с GCC дефолтным значением неинициализированного указателя не может быть trap representation, вызывающий abort() при разыменовании?

nullptr и есть такой trap.

nullptr не является дефолтным значением неинициализированного указателя.

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

дефолтным значением неинициализированного указателя.

«дефолтное значение неинициализированного указателя» - это оксюморон. неинициализированность - это мусор. случайное значение.

разумеется ни nullptr, ни какая другая константа не может быть «дефолтным значением неинициализированного значения».

вы там запутались в терминах ваще.

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

Дядя, ты на полтора десятка лет ошибся.

XT, даже я застал, пусть и уже в качестве почти музейной реликвии(а это типа 96+- год) и ещё будучи младшешколотой играющей на ней в игры.

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

Дядя, ты на полтора десятка лет ошибся.

XT, даже я застал, пусть и уже в качестве почти музейной реликвии(а это типа 96+- год) и ещё будучи младшешколотой играющей на ней в игры.

и чо? Я вот тыкаю iBook G3 двадцатилетней давности просто ради лулзов, но утверждать, что это современная система, мне совесть не позволит.

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

Дядя, ты на полтора десятка лет ошибся.

В то время (конец 80 начало 90) еще по всему миру было полно разных 16 битных и даже 8 битных компьютеров, относительная монополия совместимых с интел процессоров только начиналась.

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

Rust в Language Benchmark вполне на уровне C++ выдаёт результаты.

На уровне, но всё равно в основном процентов на 10 медленнее. И это с использованием unsafe, при наличии которого в Rust есть UB.

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

Соболезную, чо ещё сказать.

Да ладно, по сравнению с Robotron 1715 или даже отечественной EC1840 (которая типа «аналог») IBM XT на 8086 был прям монстр.

Но это проблемы максимум середины 80х годов.

А вы посмотрите на хронологию выпуска популярных операционок для PC. Windows 3.0, который еще полностью 16-битный, – это 1990-й год. Первая 32-битная OS/2 – это 1992-ой. 32-битная Windows NT – 1993-й, расширение Win32s для 16-битных Windows – что-то около 1994-го, емнип.

Хотя, казалось бы, 80386 – это 1985-й год, как раз начало работы над Windows 1.0. Если все так сразу переходили на самые новые процессоры, какой смысл был MS и IBM еще 5 лет заниматься разработкой 16-битных ОСей?

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

Практически в любой крупной кодовой базе необходима динамическая диспетчеризация.

Она не обязательно реализуется вычислимым адресом.

В терминах Си сейчас массово используют что-то типа

(*(o->class.f))(o);

Раньше писали просто

switch(o->class)
{
  case 1: f1(o); break;
  case 2: f2(o); break;
  case 3: f3(o); break;
}

Вторая конструкция (один условный переход + один вызов по фиксированному адресу) для Эльбруса гораздо эффективнее, чем вызов по адресу из данных.

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

чисто из соображений практического применения в нем должны быть штатные средства работать с переполняемой арифметикой как с отдельной сущностью

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

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

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

И это с использованием unsafe, при наличии которого в Rust есть UB.

И эти UB есть только из-за привязки к LLVM. Конечно это привязка очень многое дала для старта языка, но и проблем от нее тоже прилично, например невозможность проводить многие оптимизации которые гарантирует rust из-за особенностей и ошибок реализации LLVM и привязки оной к Си/C++. По уму расту нужен свой кодогенератор и думаю с ним он вполне мог бы потягаться и в safe режиме с Си/C++.

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

Это не динамическая диспетчеризация, а говно на палочке. Где-то на гитхабе лежал стёбный калькулятор, который «складывает» числа при помощи блоков if (a == константа && b == константа) c = константа;

Вот это решение из той же оперы.

гораздо эффективнее

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

Круг замкнулся.

https://godbolt.org/z/q8hTnEWox

https://godbolt.org/z/oj4TqWYxT

https://godbolt.org/z/xccbaM4TE

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

«Предполагаются» «небольшие»… чуешь, куда идёт?

Предполагаются – на основании чего и кем? Небольшие – это какие?

Что будет, когда «небольшие» окажутся «большими»? В какой момент это произойдёт? Сколько дыр и некорректно написанного и/или некорректно компилированного кода будет затронуто?

Это не про надёжность программ, а про гадание на кофейной гуще.

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

И генерируй неэффективный код на архитектурах, где есть поддержка бита переполнения. Ты же про эффективность сам писал. Всё, кончилась эффективность.

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

И это вместо того, чтобы дать программисту работать с переполнениями НАПРЯМУЮ.

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

И самое главное, все эти объяснения про оптимизацию – это всё притягивание за уши задним числом. Реальная причина в том, что авторы стандарта когда-то постеснялись прибить гвоздями язык к арифметике с дополнением до двойки. И до сих пор стесняются.

Хотя ежу понятно, что ничего другого, кроме дополнительного кода, int никогда ни на одной будущей платформе хранить не будет. Из соображений обратной совместимости.

Если когда-то зачем-то вдруг завезут «другие» целые знаковые числа, им дадут другое обозначение типа.

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

А вы посмотрите на хронологию выпуска популярных операционок для PC. Windows 3.0, который еще полностью 16-битный, – это 1990-й год. Первая 32-битная OS/2 – это 1992-ой. 32-битная Windows NT – 1993-й, расширение Win32s для 16-битных Windows – что-то около 1994-го, емнип.

И какая из них работала на 8086? Вот именно, никакая. Разве что только Windows 3.0 и только в реальном режиме и очень медленно, что явно не имело большого смысла.

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

И какая из них работала на 8086?

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

Я все это к тому, что на протяжении 1980-х на персоналках о производительности приходилось заботится ввиду слабости тогдашних процессоров. Все принципиально поменялось в 1990-х, а с середины 1990-х мы оказались в ситуации, когда компьютер можно (и нужно было) менять каждые два года (а то и полтора), т.к. устаревало все очень быстро. ЕМНИП, с Pentium 66MHz в 1995-ом до Pentium III 750MHz в 2000-ом. И плюс к тому, цены на все это падали еще быстрее, чем росли мегагерцы с мегабайтами.


* Возможно, как раз 80386 вместе с 80286 основную массу и составляли. Хотя тот же 80386DX-33, который уже заметно отличался по производительности от 80286 только в 1989-ом появился.

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

Вторая конструкция (один условный переход + один вызов по фиксированному адресу) для Эльбруса гораздо эффективнее, чем вызов по адресу из данных.

ну собралися специалисты.

switch и есть вызов по адресу из памяти. посмотрите как генерируется этот оператор.

  1. джамп по адресу из регистра есть лишь результат задвигания значения из регистра общего применения в регистр указателя команд.

джамп по адресу из памяти есть результат чтения значения из памяти во внутренний регистр, дальше смотри п 1(задвигание этого значения в IP). ну или прямого чтения в IP, хотя нет гарантий что архитектура умеет прямо так.

короче джамп по адресу из памяти все равно сводится к джампу по регистру.

для вызова функции все аналогично, поскольку это просто джамп, со спасением адреса возврата

alysnix ★★★
()