LINUX.ORG.RU

Почему Undefined Behaviour настолько Undefined?

 ,


0

7

Недавно поразился следующему примеру:

int const F = 1000000000;

int x = 0;

for(int y = 0; y < 7; ++y)
{
    printf("x=%d, y=%d", x, y);
    x += F; // UB
}
При компиляции с -O2 на всех компиляторах, на которых пробовал, цикл становится бесконечным, вместо ожидаемых 7 шагов. Собственно, понятно, что переполнение x влечет за собой UB, но логично предположить, что неопределено будет значение этого самого x. Так почему компилятор считает возможным выкинуть проверку счетчика цикла y, который с x никак не связан?

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

На 4.3 не воспроизводится, выше написали уже

CatsCantFly
() автор топика
Ответ на: комментарий от kirk_johnson
INT_MAX = 2^32 / 2 - 1 = 2.15e9

Если после вычислений будет значение больше, то это переполнение. Если это переполнение знакового типа, то это UB.

Константа в коде F=1e9.

После первой итерации x=1e9, все хорошо. После второй x=2e9, все хорошо. После третьей x=3e9, а это больше INT_MAX, значит, это переполнение и UB.

Компилятор считает, что программист умный, и до этого UB, как и любого другого, дело никогда не дойдет. Поэтому можно жестко оптимизировать. Раз есть UB при y=2, то условие y<7 лишнее, ведь y не может быть равно 2 (или 3). Было бы y<2, все было бы OK, а так — фигушки.

Что может компилятор сделать? Может не сделать ничего (он не обязан оптимизировать в случае UB), что и происходит на x86_64. Но он может срезать код, если где-то внутри смекнет, что это полезно.

Как такое объяснить по-человечески? А вдруг у тебя printf патченный, и каждый второй вызов делает longjump куда-нибудь еще, и из-за этого из цикла уйдем раньше, чем y достигнет 2. Или это плюсовой код, и откуда-нибудь прилетит исключение.

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

Я уже объяснил два раза, могу только повторить третий. Мозгов вам прибавить чтобы вы поняли, я увы, не могу.

1) Компилятор при оптимизации исходит из того что UB не случается. Это понятно?

2) Если исходить из того что UB не случается, значит знакового переполнения x не происходит. Это понятно?

3) Если исходить из того что знакового переполнения x не проиходит, значит 3-я итерация цикла не выполнится, потому что её выполнение неизбежно приведёт к переполнению, а значит к UB. Это понятно? Всё сказанное до сих пор вам написал сам компилятор в варнинге, если не верите. Далее оптимизатор считает что цикл не может выполниться более двух раз, точка.

4) В предположении что цикл выполнится не более двух раз, условие y < 7 всегда истинно. Это понятно?

5) Всегда истинное условие оптимизатор имеет полное право выкинуть и выкидывает. Это понятно?

Имеем бесконечный цикл. Остального когда оптимизации не коснулись, получилось что получилось - y растёт, x колбасится.

И да, на разных компиляторах, версиях, архитектурах и опциях может быть любое другое поведение, потому что оптимизаторы разные или работают по-разному, на то UB и UB. У него будет, соответственно, другое объяснение.

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

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

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

GCC now uses a more aggressive analysis to derive an upper bound for the number of iterations of loops using constraints imposed by language standards. This may cause non-conforming programs to no longer work as expected, such as SPEC CPU 2006 464.h264ref and 416.gamess. A new option, -fno-aggressive-loop-optimizations, was added to disable this aggressive analysis. In some loops that have known constant number of iterations, but undefined behavior is known to occur in the loop before reaching or during the last iteration, GCC will warn about the undefined behavior in the loop instead of deriving lower upper bound of the number of iterations for the loop. The warning can be disabled with -Wno-aggressive-loop-optimizations.

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

А он там используется? Он на всех платформах, где С компилируется, используется?

Вообще я double написал неподумавши. int x - и крэши гарантированы.

invy ★★★★★
()
Последнее исправление: invy (всего исправлений: 2)

А в асм никто не заглянул, у кого баг воспроизводится? Выложите листинг.

EXL ★★★★★
()

Интересная штука. Никогда не встречал такого поведения.

unt1tled ★★★★
()
Ответ на: комментарий от EXL
	.file	"ub.c"
	.section .rdata,"dr"
LC0:
	.ascii "x=%d, y=%d\0"
	.text
	.p2align 4,,15
	.def	_printf.constprop.0;	.scl	3;	.type	32;	.endef
_printf.constprop.0:
	subl	$28, %esp
	leal	36(%esp), %eax
	movl	%eax, 4(%esp)
	movl	$LC0, (%esp)
	call	___mingw_vprintf
	addl	$28, %esp
	ret
	.def	___main;	.scl	2;	.type	32;	.endef
	.section	.text.startup,"x"
	.p2align 4,,15
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%esi
	xorl	%esi, %esi
	pushl	%ebx
	xorl	%ebx, %ebx
	andl	$-16, %esp
	subl	$16, %esp
	call	___main
	.p2align 4,,7
L4:
	movl	%esi, 8(%esp)
	addl	$1, %esi
	movl	%ebx, 4(%esp)
	addl	$1000000000, %ebx
	movl	$LC0, (%esp)
	call	_printf.constprop.0
	jmp	L4
	.ident	"GCC: (tdm-2) 4.8.1"
	.def	___mingw_vprintf;	.scl	2;	.type	32;	.endef

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

Ты это серьёзно? Как показано неоднократно в этом треде эта логика имеет право на существование и на некоторых компиляторах работает именно так. Более того, даже на GCC твоей версии в ряде случаев будет использоваться точно такая же логика.

Вот примеры https://blogs.msdn.microsoft.com/oldnewthing/20140627-00/?p=633

Или из личного опыта (проверенное на GCC 4.8.5, 4.9.2 и 4.8.7) https://io.mndet.net/mnd/note/3Cc4T1i0Q02CiLoolvKQog

$ cat test.c
#include <stdio.h>
int main() {
  int i = 0, res = 0;
  int array[6] = {0,0,0,0,0,0};
  while ((array[i] != 6) && (i < 6)) {
    i++;
  }
  if (i == 6) {
    res = 1;
  }

  printf ("res = %d; i = %d\n", res, i);
  return 0;
}
$ gcc -O2 ./test.c
$ ./a.out 
res = 0; i = 158
$
kim-roader ★★
()
Ответ на: комментарий от kim-roader

Как показано неоднократно в этом треде эта логика имеет право на существование и на некоторых компиляторах работает именно так.

Логика при UB как у блондинки: 50/50 - или заглючит или нет.

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

... 2 раз [оставаясь при этом в рамках defined behavior], очевидно же. Естественно, реальный конпелятор не будет /специально/ танцевать румбу по поводу переполнения инта, но щит хаппенед для опа как минимум. Если не врет.

arturpub ★★
()

Сколько людей, столько UB.

arturpub ★★
()
Ответ на: комментарий от kim-roader

У Вас некорректно написан код:

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

В общем: тут программист из рода криворуких. По хорошему, этот код должен сегфолтится, т.к. тут обращение к чужой памяти.

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

Я знаю, что там кривой код. Но это real life пример. Они такие.

код должен сегфолтится

На большинстве машин не должен, всё равно никто не выделит реальной памяти меньше 8 байт, а код смотрит только в седьмой. Но главная фишка тут не в том, что есть один UB на чтение за границы массива, а в том, что GCC выкидывает проверку границ из цикла и цикл становится бесконечным (в моём случае на 158 элеметов, вместо 6)

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

Судя по файлам dump-а

 
>cat loop.c.105t.copyprop5
;; Function main (main, funcdef_no=13, decl_uid=1702, cgraph_uid=13) (executed once)

main ()
{
  int pretmp.4;
  int y;
  int x;

<bb 2>:

<bb 3>:
  # x_18 = PHI <x_6(4), 0(2)>
  # y_19 = PHI <y_7(4), 0(2)>
  __printf_chk (1, "x=%d, y=%d\n", x_18, y_19);
  x_6 = x_18 + 1000000000;
  y_7 = y_19 + 1;
  if (y_7 != 7)
    goto <bb 4>;
  else
    goto <bb 5>;

<bb 4>:
  goto <bb 3>;

<bb 5>:
  return 0;

}
Дальше идут 2 файла каких-то преобразований

>cat loop.c.111t.ivcanon
;; Function main (main, funcdef_no=13, decl_uid=1702, cgraph_uid=13) (executed once)

main ()
{
  unsigned int ivtmp.5;
  int pretmp.4;
  int y;
  int x;

<bb 2>:

<bb 3>:
  # x_18 = PHI <x_6(4), 0(2)>
  # y_19 = PHI <y_7(4), 0(2)>
  # ivtmp.5_2 = PHI <ivtmp.5_1(4), 7(2)>
  __printf_chk (1, "x=%d, y=%d\n", x_18, y_19);
  x_6 = x_18 + 1000000000;
  y_7 = y_19 + 1;
  ivtmp.5_1 = ivtmp.5_2 - 1;
  if (ivtmp.5_1 != 0)
    goto <bb 4>;
  else
    goto <bb 5>;

<bb 4>:
  goto <bb 3>;

<bb 5>:
  return 0;

}
>cat loop.c.116t.cunroll

;; Function main (main, funcdef_no=13, decl_uid=1702, cgraph_uid=13) (executed once)

main ()
{
  unsigned int ivtmp.5;
  int pretmp.4;
  int y;
  int x;

<bb 2>:

<bb 3>:
  # x_18 = PHI <x_6(4), 0(2)>
  # y_19 = PHI <y_7(4), 0(2)>
  # ivtmp.5_2 = PHI <ivtmp.5_1(4), 7(2)>
  __printf_chk (1, "x=%d, y=%d\n", x_18, y_19);
  x_6 = x_18 + 1000000000;
  y_7 = y_19 + 1;
  ivtmp.5_1 = ivtmp.5_2 - 1;
  if (ivtmp.5_1 != 0)
    goto <bb 4>;
  else
    goto <bb 5>;

<bb 4>:
  goto <bb 3>;

<bb 5>:
  return 0;

}

А дальше

>cat loop.c.120t.ivopts
;; Function main (main, funcdef_no=13, decl_uid=1702, cgraph_uid=13) (executed once)

main ()
{
  unsigned int ivtmp.5;
  int pretmp.4;
  int y;
  int x;

<bb 2>:

<bb 3>:
  # x_18 = PHI <x_6(4), 0(2)>
  # y_19 = PHI <y_7(4), 0(2)>
  __printf_chk (1, "x=%d, y=%d\n", x_18, y_19);
  x_6 = x_18 + 1000000000;
  y_7 = y_19 + 1;
  if (x_6 != -1589934592)
    goto <bb 4>;
  else
    goto <bb 5>;

<bb 4>:
  goto <bb 3>;

<bb 5>:
  return 0;

}

Тоесть в условие выхода из цикла ставится переменная x, которое в последствии выкидывается за ненадобностью

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

А он там используется? Он на всех платформах, где С компилируется, используется?

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

int x - и крэши гарантированы.

Тут не факт, но UB - само собой, хотя это уже не так важно.

hateyoufeel ★★★★★
()

Никогда не пытайся угадать логику женщины или компилятора.

anonymous
()

Никогда не используй оптимизацию.

Вообще никогда, понятно?

//школьник-кун

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.