Comments 79
Даже самый лучший компилятор не способен испортить код сильнее, чем самый плохой программист, увы.
Это точно! Но дать компилятору возможность испортить код может вообще любой программист, как бы хорош он ни был.
Хабр пал до уровня заплюсовывания псевдомудровствований. Кэп плачет навзрыд. Увы.
по поводу компиляторов выползали проблемы с ядром линукса… Оптимизировал… были случаи…
Из моей практики ARM RVCT имеет проблемы с плавающей запятой от патча к патчу отпадает функциональность, реальный баг компилятора… были проблемы в 3.x ветке
Из моей практики ARM RVCT имеет проблемы с плавающей запятой от патча к патчу отпадает функциональность, реальный баг компилятора… были проблемы в 3.x ветке
for(int i = 0; i < N; i++) *a++ = *b++ + *c++;
Первый код запутывает компилятор.
Я думаю, запутывает не только компилятор.
Да кэповская статья (я про оригинал).
Про удаление мертвого кода написано в любой заметке про компиляторы.
За конструкцию "*a++ = *b++ + *c++;" в 99 случаев из 100 можно смело бить по рукам, даже не в даваясь в оптимизацию.
За использование глобальных переменных в цикле локальной функции можно бить по рукам в 999 случаев из 1000.
Про удаление мертвого кода написано в любой заметке про компиляторы.
За конструкцию "*a++ = *b++ + *c++;" в 99 случаев из 100 можно смело бить по рукам, даже не в даваясь в оптимизацию.
За использование глобальных переменных в цикле локальной функции можно бить по рукам в 999 случаев из 1000.
Это не оптимизация — это привычка еще со времен PDP11/70, когда конструкция через указатели работала в несколько раз быстрее, чем через индексы. С тех пор руки не поднимаются использовать индексы при линейном проходе по массиву :(
За конструкцию "*a++ = *b++ + *c++;" в 99 случаев из 100 можно смело бить по рукам
Чего это вдруг? Это ж идиома для обработки массивов через указатели.
Или вы может перепутали с *a++ = *a++ +… — это да, недопустимо, т.к. UB.
Чего это вдруг? Это ж идиома для обработки массивов через указатели.
Или вы может перепутали с *a++ = *a++ +… — это да, недопустимо, т.к. UB.
Потому что профитов от этой идиомы нет.
Оптимизации нет, скорость в лучшем случае такая же, как через индексы, читабельность меньше. И ошибок в таких местах обычно тоже больше.
Оптимизации нет, скорость в лучшем случае такая же, как через индексы, читабельность меньше. И ошибок в таких местах обычно тоже больше.
У идиом профит в том что они идиомы.
Да и читаемость не меньше, а больше — именно за счет того что это идиома и ее все знают.
И с чего вы решили что оптимизации нет? Из этой статьи что ли? Ну так в комментах уже опровергли.
Про ошибки тоже спорно.
Да и читаемость не меньше, а больше — именно за счет того что это идиома и ее все знают.
И с чего вы решили что оптимизации нет? Из этой статьи что ли? Ну так в комментах уже опровергли.
Про ошибки тоже спорно.
Думаю, о читабельности кода смысла спорить нет — на вкус и цвет все фломастеры разные.
А вот с оптимизацией Вы правы. Не знаю, как там совсем старые компиляторы, но не самый свежий msvc-9.0 векторизует:
А вот с оптимизацией Вы правы. Не знаю, как там совсем старые компиляторы, но не самый свежий 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] :)
«О читабельности кода смысла спорить нет — нужно сразу бить по рукам»??? Хороший подход, прогрессивный :)
Первый код запутывает компилятор. Даже если итерации цикла независимы, компилятор не считает их таковыми; по этой причине он не может векторизовать данный кусок кода. А вот второй — может.
Что-то вы совсем древние компиляторы взяли. 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. Код полон, это всё, что относится к циклу.
Посмотрите на адреса. Команды приведены не по порядку. Видимо, дизассемблер просто сгруппировал команды, относящиеся к одной строчке.
А, ну да. Тогда получается, что последняя команда —
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
В данном случае N=10. Вот и в листинге две команды paddd, складывающие по 4 числа. А остальные два элемента компилятор добил циклом в конце (сразу после последнего movdqu в обоих листингах).
Мимо проходил. Всё обсуждаете GCC и ICC. Остальные компиляторы настолько хуже (MS, Borland)? Они не векторизуют этот цикл?
Ну, начнём с того, что компиляторы GCC и ICC есть под Linux, а MS и Borland — нету. Взглянуть на их результаты мне тоже было бы любопытно.
VS2010 не векторизует (или я не нашел нужных настроек). И код с указателями и с массивами получается совершенно одинаковым.
Я бы скорее добавил PGI к сравнению. MS и Borland никогда не славились оптимизациями.
для первого примера включить ip/ipo;
для второго добавить -fno-alias -ansi-alias в ключи компиляции;
глобальные переменные не лечатся, эт клиника…
для второго добавить -fno-alias -ansi-alias в ключи компиляции;
глобальные переменные не лечатся, эт клиника…
При выполнении DCE компилятор исключает из программы код, который никогда не выполняется.Тут вы не правы. DCE – удаление мертвого кода; кода, который никак не влияет на исполнение кода (например, неиспользуемое сложение двух чисел, или вызов процедуры без побочных эффектов, или ваш пример со сложением в цикле). UCE – удаление недостижимого кода; кода, который может иметь побочные эффекты, но не существует пути исполнения выполняющего этот код (например, код после return или ветка if-а с константым условием).
"Например, я заметил, что следующий код выполняется на 30% быстрее, если переменная N локальная, а не глобальная.
for(int i = 0; i < N; i++)
a[i] = b[i] + c[i];"
Да ну?! Какой-то плохой компилятор или неправильные настройки. Если N не volatile, то она должна быть прочитана один раз до начала выполнения цикла. Правда, только в случае, если в цикле нет вызовов не-inline процедур. По крайней мере в этом конкретном примере, никакого замедления быть не должно. Более того, даже если вызовы процедур есть, то большинство компиляторов (все?!) при любом уровне оптимизации кроме отключённой всё равно будет читать N один раз, т.к. существует общепринятое соглашение о том, в в простых циклах переменная цикла и условие цикла не должно меняться в самом цикле. Если это не так, то соответствующие опции и pragma, позволяющие данную оптимизацию отключить.
Это я про Borlang/GCC/Intel/MS.
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, то она должна быть прочитана один раз до начала выполнения цикла.
Нет. Запись в массив
Помогло бы, например, наличие модификатора const перед её объявлением.
Нет. Запись в массив
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 новым значением прямо в цикле.
Компилятору всё равно N там переменная называется или не N. Если по правилам алиасинга этот указатель может указывать на N, то один раз на цикл загружать N в регистр компилятор не имеет права.
А если по алгоритму мне нужно эту переменную поменять, то я её нужно менять — при чём тут запутывание?
Например один из вариантов: есть второй поток, который меняет N в зависимости от содержимого массива.
Разве это не UB, когда переменная N не volatile?
Нет. С чего вдруг?
Это data race, поэтому UB.
Вот цитата из стандарта:
Т.е. несинхронизированное И неатомарное чтение/запись в одну переменную из разных потоков — это гонка и UB.
Но на большинстве платформ существует широкий класс типов данных запись/чтение которых атомарно.
Например int, который рассматривается в примере с циклом — с практической точки зрения никаких гонок там не будет.
Хотя признаю, по последнему стандарту формально будет UB — поскольку вполне возможно что существует платформа где запись в int неатомарна, хотя мне такая неизвестна.
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.
Если сравнивать этот UB и например UB из второго предложенного в комментах варианта модификации N (с выходом за пределы массива), то второе — это хак и вообще неприемлим при программировании.
А вот чтение из массива целых без синхронизации из других потоков вполне применимо и безопасно для определенного класса алгоритмов.
> Десятки лет люди писали абсолютно корректные в пределах широкого круга платформ многопоточные программы которые с точки зрения нового стандарта имеют UB.
Можно пример? (Два чтения — не race.)
> А вот чтение из массива целых без синхронизации из других потоков вполне применимо и безопасно для определенного класса алгоритмов.
Стандарт писали не глупые люди: два чтения — не data race по стандарту.
Можно пример? (Два чтения — не race.)
> А вот чтение из массива целых без синхронизации из других потоков вполне применимо и безопасно для определенного класса алгоритмов.
Стандарт писали не глупые люди: два чтения — не data race по стандарту.
Я про запись в одном потоке и чтение в другом.
Ну вот если у вас такой код:
И один поток вызывает set, а второй get без синхронизации (допустим алгоритму не требуется синхронизация, например задача второго потока просто копировать куда-то значние установленное в первом потоке) — то по новому стандарту это UB.
А на практике у нас макрос детектится в configure перед сборкой и программа для левых платформ не соберется, а для остальных платформ она будет абсолютно корректной.
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 — это именно такой алгоритм.
Вот поток А пишет в цикле 1,2,3…
А поток Б читает в цикле и куда-то копирует то что удалось прочитать.
При этом алгоритму не требуется гарантировать чтение 3 если другой поток записал 3.
Устраивает любое предыдущее значение.
Переупорядочивание влияет когда от прочитанного значения зависит что делать дальше, а тут просто задача прочитать. Поэтому тут это нерелевантно.
В частности пример с модификацией N — это именно такой алгоритм.
>>А вот чтение из массива целых без синхронизации из других потоков вполне применимо и безопасно для определенного класса алгоритмов.
Это уж очень сильно платформо-зависимое утвреждение. Если они не atomic то из любой переменной в теории можно прочитать ерунду если в этот момент кто то пишет в эту переменную в другом потоке, с массивами всё ещё сложнее. Посмотрите как идёт обращение, к примеру, к массиву long (в регистрах), в общем то это имеет отношение к любому типу, не кратному размеру ячейки для текущего процессора. В новом стандарте правильно сделали, что написали — хочешь atomic, используй atomic, всё остальное не гарантируется, и это правильно, как с точки зрения возможностей оптимизации для компиятора, так и с точки зрения однозначности трактовки кроссплатформенного кода.
Это уж очень сильно платформо-зависимое утвреждение. Если они не atomic то из любой переменной в теории можно прочитать ерунду если в этот момент кто то пишет в эту переменную в другом потоке, с массивами всё ещё сложнее. Посмотрите как идёт обращение, к примеру, к массиву long (в регистрах), в общем то это имеет отношение к любому типу, не кратному размеру ячейки для текущего процессора. В новом стандарте правильно сделали, что написали — хочешь atomic, используй atomic, всё остальное не гарантируется, и это правильно, как с точки зрения возможностей оптимизации для компиятора, так и с точки зрения однозначности трактовки кроссплатформенного кода.
Да и вообще, если нет синхронизации и типы не атомарные, как это может быть не UB?
Давайте упростим код:
Очевидно, что можно вызвать f(&N, &x); где x это какая-то другая переменная.
int N;
void f(int *a, int *b) {
a[0] = b[0];
}
Очевидно, что можно вызвать f(&N, &x); где x это какая-то другая переменная.
Если быть точнее, то Pointer aliasing
void bar(int i){
return i;
}
А разве компилятор не станет ругаться на return в процедуре?
Или я чего-то не знаю о процедурах в C )
По поводу функции bar:
если это часть библиотеки, то полезно указать __attribute__((const)) в определении функции.
В этом случае GCC будет знать, что у функции нет побочных эффектов, даже если используется она в другом файле. И оптимизирует код примера.
если это часть библиотеки, то полезно указать __attribute__((const)) в определении функции.
В этом случае GCC будет знать, что у функции нет побочных эффектов, даже если используется она в другом файле. И оптимизирует код примера.
Sign up to leave a comment.
Как заставить компилятор C/C++ генерировать плохой код