Pull to refresh

Comments 31

Я только одного не понял: как это должно работать:
if(size > size+1)

В моей голове даже не возникает попытка осмыслить это. Для чего тогда вообще есть INT_MAX?!
if(size > INT_MAX-1)
INT_MAX — это максимальное значение (знакового) целого, это очень просто: http://www.cplusplus.com/reference/climits/
Если интерпретировать выражение (size > size+1) буквально, то оно равно true для любого значения, кроме INT_MAX, т.к. INT_MAX + 1 = INT_MIN из-за конечности разрядной сетки.
Но в силу того, что по стандарту переполнение знакового целого — это UB, оптимизатор заменит выражение в скобках на константу true.
Вообще-то это было среднее между сарказмом и возмущением: разве не надо быть идиотом, чтобы писать код, явно порождающий UB? Тогда и на 0 делить можно, чо…
А вот попробуете вы поддержать какой-нибудь старый HP-UX или там AIX — ещё и не такое напишите.

В частности подобный код был в свой время в скриптах autoconf'а. Собственно и сейчас там — только уже для unsigned типов.

Ага, вот поэтому то же ядро FreeBSD компилилось максимум с -O2, а то и вообще -O0 :).

А почему проверка указателя на 0 — избыточна и может быть выкинута при оптимизации?

Она избыточна, если присходит после разыменования указателя. То есть компилятор считает, что если указатель уже успешно разыменован, он не может быть нулевым.
Понятно.
Неочевидное неопределённое поведение :)

А с каких пор нельзя читать из нулевого адреса? Почему компилятор считает, что "раз читают, то адрес не может быть нулевым"?

В соответствии со стандартом, разыменование нулевого указателя является UB.

Т.е. результат взятия значения по нему является UB. По какому праву компилятор выкинул последующую проверку на 0?
Допустим нам надо написать функцию, в которую передается указатель на запись в таблице прерывания, где мы вначале сохраняем значение вектора по этому указателю, а потом, если это адрес самого первого вектора, что-то делаем. А компилировать и запускать все будем на x86, где таблица прерываний расположена по нулевому адресу. Что тогда? Все, Сишные комптляторы в топку, меняем язык?
Кстати, если я правильно понял, стандарт говорит об указателе null, а не 0. А сравнение в примере идет именно с нулем.
ИМХО, но тут явно недоработка компилятора.

Если вам нужно сделать запись в массив, расположенный по адресу 0, объявите его как volatile. Компилятор не оптимизирует доступ к volatile-переменным.
> об указателе null, а не 0
#define NULL ((void *) 0)

>ИМХО, но тут явно недоработка компилятора.
Нет.

Нулевой указатель и нулевой адрес — это и правда разные понятия. Но константа 0 обозначает именно первый. Аналогично, проверка if(ptr) проверяет именно соответствие ptr нулевому указателю а не нулевому адресу.


Т.е. результат взятия значения по нему является UB. По какому праву компилятор выкинул последующую проверку на 0?

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

UB — это такая вещь после которой разрешено любое поведение программы.
Не только «после». До — тоже можно: However, if any such execution contains an undefined operation, this International Standard places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation). (выделение моё).

Вот если программа никогда-никогда не вызвает UB — тогда и только тогда можно говорить о том, правильно её скомпилировал компилятор или нет.

Лично мне больше нравится считать С эдаким ассемблеро-заменителем. Во первых, большинство Embedded программ пишется под конкретные платформы и НЕ нуждается в переносимости. Там переносимость даже вредна. Ибо зная особенности процессора можно и нужно ими пользоваться. Во вторых, "неопределённое поведение" на самом деле вполне определённое, если мы пишем для конкретного компилятора и конкретного процессора. Я скорее за каталогизацию и документирование "определённого поведения" всех возможных компиляторов на всех возможных платформах, нежели попытку создать общий для всех и ограниченный(кастрированный), и по этому неудобный и бесполезный, сферический С в вакууме.


Конструкция (size > size + 1) идеальна! Она не зависит от разрядности числа. Компактна. Ясна. Не требует никаких констант и их определений. На мой взгляд это одна из множества жемчужин С которые надо знать и использовать.


Универсализация это всегда компромисс. Это как если у сложного и красивого многогранника сточить все грани до сферы. И самое вкусное оказывается за её пределами. К чёрту!

На мой взгляд это одна из множества жемчужин С которые надо знать и использовать.
Проблема в том, что это не жемчужина C. Это жемчужина какого-то другого языка.

Ибо зная особенности процессора можно и нужно ими пользоваться.
Не в том случае, когда вы пишите на C. Хотите сделать другой язык (ну там, EmC, или что-нибудь подобное) — вперёд с песней. Но не «мне больше нравится считать С» чем-то, чем он не является.

Чем является C — написано в стандарте языка. Точка. Он предназначен для написания переносимых программ — что значит что попытка вомпользоваться «особенностями процессора» с вероятностью 99% кончится тем, что вы вызовите UB и компилятор, рано или поздно, вам всю малину испортит.

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

В каких-то отдельных случаях происходит явное доопределение (ну как, например, с -fwrapv — но таких опций десятки, а неопределённых поведений в стандарте — сотни.
Для использования особенностей конкретного процессора есть как минимум два «законных» способа:
1) ассемблерные вставки (куда оптимизатор не лезет)
2) intrinsic-функции, которые для этого и существуют.
Это уже, как бы, не совсем C. Но, как бы, в случаях, когда разработчики компилятора явно предусмотрели отход от стандарта — например определили тип __m128 как float __attribute__ ((__vector_size__ (16), __may_alias__)) — то и использовании этих вещей в соответствии с описаниями не будет UB.

Holix же выступает за совсем другое: без явного документирования особенностей использовать определённые UB как если бы они не были UB — исходя из того, что реально сегодня, сейчас, делает компилятор.

Это — путь в никуда: сегодня ваша программа собралась и, возможно, даже работает — но никто не может гарантировать что завтра, при выходе небольшой правки к компилятору (а иногда и к процессору — там тоже UB бывают, как это ни удивительно) все «не слетит с катушек».
«неопределённое поведение» на самом деле вполне определённое, если мы пишем для конкретного компилятора и конкретного процессора
Вы с implementation defined behavior не путаете часом?
Путает, конечно. Я так думаю, что стандарт он открывать и читать даже не пытался, пишет «по наитию».
1. Нет, не путаю.
2. Компилятор, внезапно, имеет абсолютно детерминированный алгоритм оптимизации, который в каждом конкретном случае приведёт к конкретному поведению.
3. На эту тему можно долго и много рассуждать, скоро напишу ещё, кстати.
1. Нет, не путаю.
Таки путаете.
2. Компилятор, внезапно, имеет абсолютно детерминированный алгоритм оптимизации, который в каждом конкретном случае приведёт к конкретному поведению.
Внезапно компилятор — это ещё не всё. Рассмотрите классическую программу с UB:
  int *p = new int[10];
  ...
  delete[] p;
  ...
  p[5] = 120; // Что случится здесь?
Или ещё лучше:
  int *p = new int[10];
  p[-2] = 17;

Поведение программы может зависеть от массы разных причин — особенно на современных системах с ASLR. И компилятор ничего с этим сделать не может.
3. На эту тему можно долго и много рассуждать, скоро напишу ещё, кстати.
Давайте-давайте. Начните с пары прамеров, описанных выше, расскажите про ваш магический компилятор (у вас же он припасён в рукаве, правда?), который что-то там конкретное гарантирует, а мы будем рассматривать его в разных системах и с разными libc, tcmalloc'ами и прочим.

Только боюсь не выйдет у вас ничего. Из нескольких сотен описанных в стандарте UB компилятор может отловить (и отлавливает — см. UBSAN) дай бог несколько десятков — и то негарантированно.
Вы бы не так по-хамски общались, можно было бы поговорить на эту тему.
Вот бы было здорово, если бы при компиляции IDE сразу бы могла показать код который изменится при оптимизации. Написал разименование а потом проверку на ноль, а IDE сразу сделал код серым и сразу ясно что, что-то не то.
В следующей части как раз написано, в чём тут сложность.
С какой то из последних версий GCC(G++) вам рассказывает о «ненужных» проверках на нуль.
Удивлен что
1) не было упомянуто семейство *sanitizer (https://github.com/google/sanitizers), clang.llvm.org/docs/UndefinedBehaviorSanitizer.html Уже несколько лет регулярно пользуемся ими на дебажных билдах при прогоне тестов. Отловили в тестах приличное количество проблем которые или себя никак не проявляли или проявляли себя спорадически.
2) до сих пор кто-то использует супер медленный и тормозной valgrind. Что-то более или менее интенсивно потребляющее CPU или память под ним запустить практически невозможно. Asan же дает всего 2x замедление и ~2x потребление памяти. Еще одна проблема valgrind в том что множество юзкейсов связанных с таймингом на нем не воспроизводятся.

На URL оригинальной статьи посмотрите, да? Хинт: 2011 — это не номер авто автора.
Да, не обратил внимание. ИМХО с точки зрения переводчика стоило бы актуализировать некоторую информацию (для IT 6 лет это огромный срок). Тот же klee был одно время заброшен полностью. Сейчас пытаются проект возродить (судя по офиц. сайту), однако тот факт что он до сих пор собирается с llvm 3.4 несколько удручает. Опять таки, стоило бы добавить информацию про проекты упомянутые мною выше.
Sign up to leave a comment.

Articles