Как стать автором
Обновить

Комментарии 79

Даже самый лучший компилятор не способен испортить код сильнее, чем самый плохой программист, увы.
Это точно! Но дать компилятору возможность испортить код может вообще любой программист, как бы хорош он ни был.
Хабр пал до уровня заплюсовывания псевдомудровствований. Кэп плачет навзрыд. Увы.
по поводу компиляторов выползали проблемы с ядром линукса… Оптимизировал… были случаи…

Из моей практики ARM RVCT имеет проблемы с плавающей запятой от патча к патчу отпадает функциональность, реальный баг компилятора… были проблемы в 3.x ветке
Да кэповская статья (я про оригинал).
Про удаление мертвого кода написано в любой заметке про компиляторы.
За конструкцию "*a++ = *b++ + *c++;" в 99 случаев из 100 можно смело бить по рукам, даже не в даваясь в оптимизацию.
За использование глобальных переменных в цикле локальной функции можно бить по рукам в 999 случаев из 1000.

Это не оптимизация — это привычка еще со времен PDP11/70, когда конструкция через указатели работала в несколько раз быстрее, чем через индексы. С тех пор руки не поднимаются использовать индексы при линейном проходе по массиву :(
За конструкцию "*a++ = *b++ + *c++;" в 99 случаев из 100 можно смело бить по рукам

Чего это вдруг? Это ж идиома для обработки массивов через указатели.

Или вы может перепутали с *a++ = *a++ +… — это да, недопустимо, т.к. UB.
Потому что профитов от этой идиомы нет.
Оптимизации нет, скорость в лучшем случае такая же, как через индексы, читабельность меньше. И ошибок в таких местах обычно тоже больше.
У идиом профит в том что они идиомы.
Да и читаемость не меньше, а больше — именно за счет того что это идиома и ее все знают.

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

Про ошибки тоже спорно.
Думаю, о читабельности кода смысла спорить нет — на вкус и цвет все фломастеры разные.

А вот с оптимизацией Вы правы. Не знаю, как там совсем старые компиляторы, но не самый свежий msvc-9.0 векторизует:

Раз
for(int i = 0; i < N; i++)
00161049  mov         edx,dword ptr [esp+10h] 
0016104D  lea         esi,[eax+10h] 
00161050  add         edx,8 
00161053  sub         eax,dword ptr [esp+10h] 
00161057  lea         ecx,[argc] 
0016105A  mov         dword ptr [esp+10h],14h 
	{
		a[i] = b[i] + c[i];
00161062  mov         ebp,dword ptr [ecx-4] 
00161065  add         ebp,dword ptr [esi-10h] 
00161068  add         ecx,14h 
0016106B  mov         dword ptr [edx-8],ebp 
0016106E  mov         ebp,dword ptr [ecx+ebx-14h] 
00161072  add         ebp,dword ptr [ecx-14h] 
00161075  add         edx,14h 
00161078  mov         dword ptr [ecx+edi-14h],ebp 
0016107C  mov         ebp,dword ptr [eax+edx-14h] 
00161080  add         ebp,dword ptr [ecx-10h] 
00161083  add         esi,14h 
00161086  mov         dword ptr [edx-14h],ebp 
00161089  mov         ebp,dword ptr [ecx-0Ch] 
0016108C  add         ebp,dword ptr [esi-18h] 
0016108F  mov         dword ptr [edx-10h],ebp 
00161092  mov         ebp,dword ptr [ecx-8] 
00161095  add         ebp,dword ptr [esi-14h] 
00161098  sub         dword ptr [esp+10h],1 
0016109D  mov         dword ptr [edx-0Ch],ebp 
001610A0  jne         main+62h (161062h) 
001610A2  pop         edi  
001610A3  pop         esi  
001610A4  pop         ebp  
	}
	a += N;
	b += N;
	c += N;

Два
for (int i = 0; i < N; i++)
00191046  mov         ecx,14h 
0019104B  jmp         main+50h (191050h) 
0019104D  lea         ecx,[ecx] 
	{
		*a++ = *b++ + *c++;
00191050  mov         edx,dword ptr [esi] 
00191052  add         edx,dword ptr [eax] 
00191054  add         edi,14h 
00191057  mov         dword ptr [edi-14h],edx 
0019105A  mov         edx,dword ptr [eax+4] 
0019105D  add         edx,dword ptr [esi+4] 
00191060  add         esi,14h 
00191063  mov         dword ptr [edi-10h],edx 
00191066  mov         edx,dword ptr [eax+8] 
00191069  add         edx,dword ptr [esi-0Ch] 
0019106C  add         eax,14h 
0019106F  mov         dword ptr [edi-0Ch],edx 
00191072  mov         edx,dword ptr [eax-8] 
00191075  add         edx,dword ptr [esi-8] 
00191078  mov         dword ptr [edi-8],edx 
0019107B  mov         edx,dword ptr [eax-4] 
0019107E  add         edx,dword ptr [esi-4] 
00191081  sub         ecx,1 
00191084  mov         dword ptr [edi-4],edx 
00191087  jne         main+50h (191050h) 
00191089  pop         edi  
0019108A  pop         esi  
0019108B  pop         ebp  
	}
И где же здесь векторизация? В лучшем случае, объединение нескольких шагов цикла. Особенно интересна команда lea ecx,[ecx] :)
Прошу прощения, я про оптимизацию, конечно, а не про векторизацию.
С ключом /O2 код получается схожим.

Автоматическая векторизация же ни в msvc-9.0, ни в msvc-10.0 не поддерживается, так что тут обсуждать нечего.
«О читабельности кода смысла спорить нет — нужно сразу бить по рукам»??? Хороший подход, прогрессивный :)
Первый код запутывает компилятор. Даже если итерации цикла независимы, компилятор не считает их таковыми; по этой причине он не может векторизовать данный кусок кода. А вот второй — может.

Что-то вы совсем древние компиляторы взяли. GCC 4.7 спокойно векторизует этот код, предварительно вставляя проверку, что a, b и c не накладываются друг на друга в пределах sizeof(*a).
10: create runtime check for data references *b_27 and *a_26
10: create runtime check for data references *c_28 and *a_26

Ну а чтобы он и этой проверки не делал, можно указать __restrict__, тогда этот цикл будет векторизован без всякой ругани.
void f(int * __restrict__ a, int * __restrict__ b, int * __restrict__ c, int N) {
  for(int i = 0; i < N; i++)
    *a++ = *b++ + *c++;
}

Кстати, чтобы посмотреть весь ход автовекторизации, есть отличные опции: g++ -O2 -ftree-vectorize -march=native -ftree-vectorizer-verbose=5
Да, вы правы. Я проверил — оба варианта действительно вектоpизуются. Только код получается немного разным:

Скрытый текст
44			for (int i=0; i<N; i++)
   0x0000000000400828 <+536>:	cmp    %rsi,%rdx
   0x000000000040082b <+539>:	jne    0x400818 <main()+520>
   0x000000000040082d <+541>:	lea    0x28(%r12),%rbx
45			{
46				*a++ = *b++ + *c++;
   0x00000000004007c9 <+441>:	movdqu 0x0(%rbp),%xmm1
   0x00000000004007ce <+446>:	lea    0x20(%r12),%rax
   0x00000000004007d3 <+451>:	mov    $0x2,%edx
   0x00000000004007d8 <+456>:	movdqu 0x0(%r13),%xmm0
   0x00000000004007de <+462>:	paddd  %xmm1,%xmm0
   0x00000000004007e2 <+466>:	movdqu %xmm0,(%r12)
   0x00000000004007e8 <+472>:	movdqu 0x10(%rbp),%xmm1
   0x00000000004007ed <+477>:	add    $0x20,%rbp
   0x00000000004007f1 <+481>:	movdqu 0x10(%r13),%xmm0
   0x00000000004007f7 <+487>:	add    $0x20,%r13
   0x00000000004007fb <+491>:	paddd  %xmm1,%xmm0
   0x00000000004007ff <+495>:	movdqu %xmm0,0x10(%r12)
   0x0000000000400818 <+520>:	mov    0x0(%rbp,%rdx,1),%ecx
   0x000000000040081c <+524>:	add    0x0(%r13,%rdx,1),%ecx
   0x0000000000400821 <+529>:	mov    %ecx,(%rax,%rdx,1)
   0x0000000000400824 <+532>:	add    $0x4,%rdx
47			}



Скрытый текст
20			for (int i=0; i<N; i++)
   0x00000000004006bc <+172>:	mov    $0x8,%eax
   0x0000000000400710 <+256>:	cmp    %rsi,%rdx
   0x0000000000400713 <+259>:	jne    0x400700 <main()+240>
21			{
22				a[i] = b[i]+c[i];
   0x00000000004006b1 <+161>:	movdqu 0x0(%r13),%xmm1
   0x00000000004006b7 <+167>:	mov    $0x2,%edx
   0x00000000004006c1 <+177>:	movdqu (%r12),%xmm0
   0x00000000004006c7 <+183>:	paddd  %xmm1,%xmm0
   0x00000000004006cb <+187>:	movdqu %xmm0,0x0(%rbp)
   0x00000000004006d0 <+192>:	movdqu 0x10(%r13),%xmm1
   0x00000000004006d6 <+198>:	movdqu 0x10(%r12),%xmm0
   0x00000000004006dd <+205>:	paddd  %xmm1,%xmm0
   0x00000000004006e1 <+209>:	movdqu %xmm0,0x10(%rbp)
   0x0000000000400700 <+240>:	mov    0x0(%r13,%rdx,1),%ecx
   0x0000000000400705 <+245>:	add    (%r12,%rdx,1),%ecx
   0x0000000000400709 <+249>:	mov    %ecx,(%rax,%rdx,1)
   0x000000000040070c <+252>:	add    $0x4,%rdx
23			}



Насколько я понимаю, второй вариант всё-таки будет эффективнее.
А чем вы так красиво сгенерировали листинг? -fdump-tree-optimized выводит оптимизированную версию с внутреннего представления, -g -Wa,-ahl=test.s ссылается не на все строки, а -S -fverbose-asm ссылается не на реальные строки, а опять же на внутреннее представление.
Может ли кто-нибудь объяснить, где здесь цикл? Явной команды перехода не заметно — может быть, она скрыта другими конструкциями? И что происходит, если N не делится на 4?
Просто код не весь.
Тогда ответ на второй вопрос особенно интересен.
В данном примере N = 10. Код полон, это всё, что относится к циклу.
А как выглядят те же функции, когда информации про N нет? По-прежнему код с указателями запутывает компилятор сильнее?
Даже в этом случае возможна частичная размотка цикла, с обработкой остатка от деления на 4 отдельным участком кода.
Посмотрите на адреса. Команды приведены не по порядку. Видимо, дизассемблер просто сгруппировал команды, относящиеся к одной строчке.
А, ну да. Тогда получается, что последняя команда — jne, и вопpос «где цикл?» отпадает сам собой.
Действительно. Интересно, куда делся код со смещениями 500-520 и 210-240. В нём тоже могло что-нибудь происходить.
Без /m:

С указателями:

Скрытый текст
   0x0000000000400a7a <+426>:	add    $0x4,%rbx
   0x0000000000400a7e <+430>:	cmp    $0x28,%rbx
   0x0000000000400a82 <+434>:	jne    0x400a50 <main()+384>
   0x0000000000400a84 <+436>:	lea    0x10(%r12),%rax
   0x0000000000400a89 <+441>:	lea    0x10(%r13),%rdx
   0x0000000000400a8d <+445>:	cmp    %rax,%r13
   0x0000000000400a90 <+448>:	setae  %cl
   0x0000000000400a93 <+451>:	cmp    %rdx,%r12
   0x0000000000400a96 <+454>:	setae  %dl
   0x0000000000400a99 <+457>:	or     %dl,%cl
   0x0000000000400a9b <+459>:	je     0x400b60 <main()+656>
   0x0000000000400aa1 <+465>:	cmp    %rax,%rbp
   0x0000000000400aa4 <+468>:	lea    0x10(%rbp),%rax
   0x0000000000400aa8 <+472>:	setae  %dl
   0x0000000000400aab <+475>:	cmp    %rax,%r12
   0x0000000000400aae <+478>:	setae  %al
   0x0000000000400ab1 <+481>:	or     %al,%dl
   0x0000000000400ab3 <+483>:	je     0x400b60 <main()+656>
   0x0000000000400ab9 <+489>:	movdqu 0x0(%rbp),%xmm1
   0x0000000000400abe <+494>:	lea    0x20(%r12),%rax
   0x0000000000400ac3 <+499>:	mov    $0x2,%edx
   0x0000000000400ac8 <+504>:	movdqu 0x0(%r13),%xmm0
   0x0000000000400ace <+510>:	paddd  %xmm1,%xmm0
   0x0000000000400ad2 <+514>:	movdqu %xmm0,(%r12)
   0x0000000000400ad8 <+520>:	movdqu 0x10(%rbp),%xmm1
   0x0000000000400add <+525>:	add    $0x20,%rbp
   0x0000000000400ae1 <+529>:	movdqu 0x10(%r13),%xmm0
   0x0000000000400ae7 <+535>:	add    $0x20,%r13
   0x0000000000400aeb <+539>:	paddd  %xmm1,%xmm0
   0x0000000000400aef <+543>:	movdqu %xmm0,0x10(%r12)
   0x0000000000400af6 <+550>:	sub    $0x1,%edx
   0x0000000000400af9 <+553>:	lea    0x4(,%rdx,4),%rsi
   0x0000000000400b01 <+561>:	xor    %edx,%edx
   0x0000000000400b03 <+563>:	nopl   0x0(%rax,%rax,1)
   0x0000000000400b08 <+568>:	mov    0x0(%rbp,%rdx,1),%ecx
   0x0000000000400b0c <+572>:	add    0x0(%r13,%rdx,1),%ecx
   0x0000000000400b11 <+577>:	mov    %ecx,(%rax,%rdx,1)
   0x0000000000400b14 <+580>:	add    $0x4,%rdx



С индексами:

Скрытый текст
   0x0000000000400962 <+146>:	add    $0x4,%rbx
   0x0000000000400966 <+150>:	cmp    $0x28,%rbx
   0x000000000040096a <+154>:	jne    0x400938 <main()+104>
   0x000000000040096c <+156>:	lea    0x10(%rbp),%rax
   0x0000000000400970 <+160>:	lea    0x10(%r12),%rdx
   0x0000000000400975 <+165>:	cmp    %rax,%r12
   0x0000000000400978 <+168>:	setae  %cl
   0x000000000040097b <+171>:	cmp    %rdx,%rbp
   0x000000000040097e <+174>:	setae  %dl
   0x0000000000400981 <+177>:	or     %dl,%cl
   0x0000000000400983 <+179>:	je     0x400b54 <main()+644>
   0x0000000000400989 <+185>:	cmp    %rax,%r13
   0x000000000040098c <+188>:	lea    0x10(%r13),%rax
   0x0000000000400990 <+192>:	setae  %dl
   0x0000000000400993 <+195>:	cmp    %rax,%rbp
   0x0000000000400996 <+198>:	setae  %al
   0x0000000000400999 <+201>:	or     %al,%dl
   0x000000000040099b <+203>:	je     0x400b54 <main()+644>
   0x00000000004009a1 <+209>:	movdqu 0x0(%r13),%xmm1
   0x00000000004009a7 <+215>:	mov    $0x2,%edx
   0x00000000004009ac <+220>:	mov    $0x8,%eax
   0x00000000004009b1 <+225>:	movdqu (%r12),%xmm0
   0x00000000004009b7 <+231>:	paddd  %xmm1,%xmm0
   0x00000000004009bb <+235>:	movdqu %xmm0,0x0(%rbp)
   0x00000000004009c0 <+240>:	movdqu 0x10(%r13),%xmm1
   0x00000000004009c6 <+246>:	movdqu 0x10(%r12),%xmm0
   0x00000000004009cd <+253>:	paddd  %xmm1,%xmm0
   0x00000000004009d1 <+257>:	movdqu %xmm0,0x10(%rbp)
   0x00000000004009d6 <+262>:	shl    $0x2,%rax
   0x00000000004009da <+266>:	sub    $0x1,%edx
   0x00000000004009dd <+269>:	lea    0x4(,%rdx,4),%rsi
   0x00000000004009e5 <+277>:	add    %rax,%r13
   0x00000000004009e8 <+280>:	add    %rax,%r12
   0x00000000004009eb <+283>:	xor    %edx,%edx
   0x00000000004009ed <+285>:	add    %rbp,%rax
   0x00000000004009f0 <+288>:	mov    0x0(%r13,%rdx,1),%ecx
   0x00000000004009f5 <+293>:	add    (%r12,%rdx,1),%ecx
   0x00000000004009f9 <+297>:	mov    %ecx,(%rax,%rdx,1)
   0x00000000004009fc <+300>:	add    $0x4,%rdx

Любопытно, что в случае массивов он так же аккуратно проверяет перекрытия, как и в случае индексов. Интересно, зачем ему вся возня с %edx.
В данном случае N=10. Вот и в листинге две команды paddd, складывающие по 4 числа. А остальные два элемента компилятор добил циклом в конце (сразу после последнего movdqu в обоих листингах).
Мимо проходил. Всё обсуждаете GCC и ICC. Остальные компиляторы настолько хуже (MS, Borland)? Они не векторизуют этот цикл?
Ну, начнём с того, что компиляторы GCC и ICC есть под Linux, а MS и Borland — нету. Взглянуть на их результаты мне тоже было бы любопытно.
VS2010 не векторизует (или я не нашел нужных настроек). И код с указателями и с массивами получается совершенно одинаковым.
Я бы скорее добавил PGI к сравнению. MS и Borland никогда не славились оптимизациями.
для первого примера включить ip/ipo;
для второго добавить -fno-alias -ansi-alias в ключи компиляции;
глобальные переменные не лечатся, эт клиника…
При выполнении DCE компилятор исключает из программы код, который никогда не выполняется.
Тут вы не правы. DCE – удаление мертвого кода; кода, который никак не влияет на исполнение кода (например, неиспользуемое сложение двух чисел, или вызов процедуры без побочных эффектов, или ваш пример со сложением в цикле). UCE – удаление недостижимого кода; кода, который может иметь побочные эффекты, но не существует пути исполнения выполняющего этот код (например, код после return или ветка if-а с константым условием).
В определении действительно была ошибка. Исправил. Почему вы не написали в личку?
Я хотел поделиться со всеми знанием об отличиях DCE и UCE, так как недавно и сам их путал.
Тогда извините.
"Например, я заметил, что следующий код выполняется на 30% быстрее, если переменная N локальная, а не глобальная.

for(int i = 0; i < N; i++)
a[i] = b[i] + c[i];
"
Да ну?! Какой-то плохой компилятор или неправильные настройки. Если N не volatile, то она должна быть прочитана один раз до начала выполнения цикла. Правда, только в случае, если в цикле нет вызовов не-inline процедур. По крайней мере в этом конкретном примере, никакого замедления быть не должно. Более того, даже если вызовы процедур есть, то большинство компиляторов (все?!) при любом уровне оптимизации кроме отключённой всё равно будет читать N один раз, т.к. существует общепринятое соглашение о том, в в простых циклах переменная цикла и условие цикла не должно меняться в самом цикле. Если это не так, то соответствующие опции и pragma, позволяющие данную оптимизацию отключить.
Это я про Borlang/GCC/Intel/MS.
Пробуем на gcc 4.7.2 c -O2:

Скрытый текст
   0x00000000004008d6 <+6>:	mov    $0xa,%ebx
   0x00000000004008db <+11>:	sub    $0x8,%rsp
   0x00000000004008df <+15>:	nop
16		{
17			int N = 10;
18			for (int i=0; i<N; i++)
   0x00000000004008e5 <+21>:	sub    $0x1,%ebx
   0x00000000004008e8 <+24>:	jne    0x4008e0 <main()+16>
19			{
20				doSomething();
   0x00000000004008e0 <+16>:	callq  0x400b90 <doSomething()>
21			}



Скрытый текст
24			for (int i=0; i<globalN; i++)
   0x00000000004008ea <+26>:	mov    0x2009d0(%rip),%eax        # 0x6012c0 <globalN>
   0x00000000004008f0 <+32>:	xor    %ebx,%ebx
   0x00000000004008f2 <+34>:	test   %eax,%eax
   0x00000000004008f4 <+36>:	jle    0x400910 <main()+64>
   0x00000000004008f6 <+38>:	nopw   %cs:0x0(%rax,%rax,1)
   0x0000000000400905 <+53>:	add    $0x1,%ebx
   0x0000000000400908 <+56>:	cmp    %ebx,0x2009b2(%rip)        # 0x6012c0 <globalN>
   0x000000000040090e <+62>:	jg     0x400900 <main()+48>
25			{
26				doSomething();
   0x0000000000400900 <+48>:	callq  0x400b90 <doSomething()>

27			}



Чувствуете разницу? В случае с локальной переменной вообще нет обращений к памяти.
А если объявить N и globalN как const?
Тогда одинаково. Но на практике длина массива далеко не всегда является константой, и локальная переменная создаёт больший простор для оптимизации.
А если, в случае С++, сделать ее членом класса и использовать внутри метода?
Ну очевидно же — оптимизироваться будет не так плохо, как глобальная, но хуже, чем локальная. Если она private и к ней обращается только один метод — случай аналогичен локальной (не уверен, что любой компилятор это поймёт, но вышеупомянутый gcc понял).
Небольшое уточнение: на количество чтений влияет не столько локальность/глобальность переменных, сколько локальность/глобальность привязки. Если перемененная будет определена вне функции, но со словом static, то компилятор с лёгкостью определит оптимальное количество чтений. Это важно. На практике, если уж без глобальных переменных не обойтись, всё равно почти всегда переменную удаётся сделать статической и обращаться к ней извне через специальные функции.
> Если N не volatile, то она должна быть прочитана один раз до начала выполнения цикла.
Нет. Запись в массив a[i] тоже может изменить значение N. Поэтому «в общем случае» её надо перечитывать каждую итерацию цикла.
Помогло бы, например, наличие модификатора const перед её объявлением.
Каким образом запись a[i] может изменить значение N?
Код
#include <iostream>
int main()
{
    int lsize = 2;
    int array[2];
    int k = -1;
    std::cout<<array+k<<" "<<&lsize<<" "<<lsize<<std::endl;
    array[k] = 100500;
    std::cout<<array+k<<" "<<&lsize<<" "<<lsize<<std::endl;
    return 0;
}

Вывод на codepad.
Для разных компиляторов значение k может быть разным, но главное мы можем, обратившись к ячейке массива по индексу, перезаписать область памяти, которая формально этому массиву не принадлежит, и ничего нам за это не будет.
Это все-таки UB. А есть и вполне легальные варианты.
Конечно это UB. Разумеется так писать нельзя. Оба примера не являются легальными вариантами. Не существует легального варианта. Нельзя писать код, поведение которого мы не можем предсказать.
Но мы живём не в идеальном мире и на практике…
Никто не говорил, что значение N изменить нельзя или что сделать это сложно, да и примеров хватает, но это не безопасно. Очень просто запутать себя, товарища, компилятор, пользователя, данные, если перезаписывать N новым значением прямо в цикле.
> Никто не говорил, что значение N изменить нельзя или что сделать это сложно
>> Не существует легального варианта.

Вы же и говорили. Вы бы хоть не противоречили себе в соседних постах?

> Очень просто запутать себя, товарища, компилятор, пользователя, данные, если перезаписывать N новым значением прямо в цикле.

Компилятору всё равно N там переменная называется или не N. Если по правилам алиасинга этот указатель может указывать на N, то один раз на цикл загружать N в регистр компилятор не имеет права.

А если по алгоритму мне нужно эту переменную поменять, то я её нужно менять — при чём тут запутывание?
Например один из вариантов: есть второй поток, который меняет N в зависимости от содержимого массива.
Разве это не UB, когда переменная N не volatile?
Нет. С чего вдруг?
Это data race, поэтому UB.
Вот цитата из стандарта:
The execution of a program contains a data race if it contains two conflicting actions in
different threads, at least one of which is not atomic, and neither happens before the
other. Any such data race results in undefined behavior.

Т.е. несинхронизированное И неатомарное чтение/запись в одну переменную из разных потоков — это гонка и UB.

Но на большинстве платформ существует широкий класс типов данных запись/чтение которых атомарно.
Например int, который рассматривается в примере с циклом — с практической точки зрения никаких гонок там не будет.

Хотя признаю, по последнему стандарту формально будет UB — поскольку вполне возможно что существует платформа где запись в int неатомарна, хотя мне такая неизвестна.
Под 'atomic' подразамувается не что угодно, и не то что процессору взбредёт в голову, а то, что определено в clause [atomics].
Я понимаю.
Но есть стандарт, а есть стандарт дефакто.
Десятки лет люди писали абсолютно корректные в пределах широкого круга платформ многопоточные программы которые с точки зрения нового стандарта имеют UB.

Если сравнивать этот UB и например UB из второго предложенного в комментах варианта модификации N (с выходом за пределы массива), то второе — это хак и вообще неприемлим при программировании.
А вот чтение из массива целых без синхронизации из других потоков вполне применимо и безопасно для определенного класса алгоритмов.
> Десятки лет люди писали абсолютно корректные в пределах широкого круга платформ многопоточные программы которые с точки зрения нового стандарта имеют UB.

Можно пример? (Два чтения — не race.)

> А вот чтение из массива целых без синхронизации из других потоков вполне применимо и безопасно для определенного класса алгоритмов.

Стандарт писали не глупые люди: два чтения — не data race по стандарту.

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

Ну вот если у вас такой код:
int v;
void set(int a)
{
   static_assert(PLALTFORM_HAS_ATOMIC_INT_STORE);
   v = a;
}
int get()
{
   static_assert(PLALTFORM_HAS_ATOMIC_INT_STORE);
   return v;
}


И один поток вызывает set, а второй get без синхронизации (допустим алгоритму не требуется синхронизация, например задача второго потока просто копировать куда-то значние установленное в первом потоке) — то по новому стандарту это UB.
А на практике у нас макрос детектится в configure перед сборкой и программа для левых платформ не соберется, а для остальных платформ она будет абсолютно корректной.
Вы понимаете что v = a; не гарантирует что будет store в память? Инлайнинг и переупорядочивание записей сделают своё дело и этот стор может быть вынесен да практически куда угодно.
Так я ж написал — алгоритму не нужна синхронизация.
Вот поток А пишет в цикле 1,2,3…
А поток Б читает в цикле и куда-то копирует то что удалось прочитать.
При этом алгоритму не требуется гарантировать чтение 3 если другой поток записал 3.
Устраивает любое предыдущее значение.

Переупорядочивание влияет когда от прочитанного значения зависит что делать дальше, а тут просто задача прочитать. Поэтому тут это нерелевантно.

В частности пример с модификацией N — это именно такой алгоритм.
Я могу только сказать одно: удачи с отладкой. #define true false и то проще найти. И ещё, удачи с использованием современных инструментов определения data race.
Найти отладкой что?
Что данные не точны — то что изначально известно при построении алгоритма?

>>А вот чтение из массива целых без синхронизации из других потоков вполне применимо и безопасно для определенного класса алгоритмов.

Это уж очень сильно платформо-зависимое утвреждение. Если они не atomic то из любой переменной в теории можно прочитать ерунду если в этот момент кто то пишет в эту переменную в другом потоке, с массивами всё ещё сложнее. Посмотрите как идёт обращение, к примеру, к массиву long (в регистрах), в общем то это имеет отношение к любому типу, не кратному размеру ячейки для текущего процессора. В новом стандарте правильно сделали, что написали — хочешь atomic, используй atomic, всё остальное не гарантируется, и это правильно, как с точки зрения возможностей оптимизации для компиятора, так и с точки зрения однозначности трактовки кроссплатформенного кода.
Да и вообще, если нет синхронизации и типы не атомарные, как это может быть не UB?
volatile то тут причем?
Непричём, я ерунду написал.
Давайте упростим код:
int N;
void f(int *a, int *b) {
  a[0] = b[0];
}


Очевидно, что можно вызвать f(&N, &x); где x это какая-то другая переменная.
Это называется алиазинг. Согласно стандарту, алиазинг возможен между указателями p1 и p2, если совпадают типы p1[0] и p2[0]. Если типы целочисленные и отличаются только знаком (signed/unsigned), алиазинг тоже возможен. Указатель на char «алиазится» с чем угодно.
Если быть точнее, то Pointer aliasing
void bar(int i){
  return i;
}

А разве компилятор не станет ругаться на return в процедуре?
Или я чего-то не знаю о процедурах в C )
Станет. Будет ругаться примерно 10 секунд, пока ошибка не будет исправлена. На результат это почти не повлияет.
Ага. Исправил.
По поводу функции bar:
если это часть библиотеки, то полезно указать __attribute__((const)) в определении функции.

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

Публикации