Ответ 1
Поведение ICC здесь не является доказательством UB в ISO C++ или C. Я думаю, что ваши рассуждения правильны, и это хорошо определено. Вы нашли ошибку ICC. Если кому-то все равно, сообщите об этом на своих форумах: https://software.intel.com/en-us/forums/intel-c-compiler. Существующие сообщения об ошибках в этом разделе их форума были приняты разработчиками, например, этим.
Мы можем построить пример, где он автоматически векторизуется тем же способом (с безусловным и неатомарным чтением/возможно-изменением/перезаписью), где это явно недопустимо, потому что чтение/перезапись происходит на 2-й строке, которую абстрактная машина C не делает. даже не читал.
Таким образом, мы не можем доверять генератору кода ICC, чтобы сообщить нам что-нибудь о том, когда мы вызвали UB, потому что это сделает аварийный код даже в явно законных случаях.
void replace(const+Char+*str1,+Char+*str2, size_t len) {%0A++++for (size_t я %3D+0; я < len; i++) {%0A++++++++if (str1[i%5D+=%3D+!'/!') {%0A++++++++ str2[i%5D+%3D+!'_!';%0A++++++++}%0A++++}
}'),l:'5',n:'0',o:'C++ source #1',t:'0')),k:46.80860681674351,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:icc191,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),lang:c++,libs:!(),options:'-O2 -std=gnu+%2B11+-march=skylake',source:1),l:'5',n:'0',o:'x86-64 icc 19.0.1+(Editor #1,+Compiler+#1)+C++',t:'0')),k:53.19139318325648,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4 rel="nofollow noreferrer">Godbolt: ICC19.0.1 -O2 -march=skylake
(Старая -O2 -march=skylake
ICC понимала только такие параметры, как -xcore-avx2
, но современная ICC понимает ту же -march
что и GCC/clang.)
#include <stddef.h>
void replace(const char *str1, char *str2, size_t len) {
for (size_t i = 0; i < len; i++) {
if (str1[i] == '/') {
str2[i] = '_';
}
}
}
Он проверяет перекрытие между str1[0..len-1]
и str2[0..len-1]
, но для достаточно большого len
и без перекрытия он будет использовать этот внутренний цикл:
..B1.15: # Preds ..B1.15 ..B1.14 //do{
vmovdqu ymm2, YMMWORD PTR [rsi+r8] #6.13 // load from str2
vpcmpeqb ymm3, ymm0, YMMWORD PTR [rdi+r8] #5.24 // compare vs. str1
vpblendvb ymm4, ymm2, ymm1, ymm3 #6.13 // blend
vmovdqu YMMWORD PTR [r8+rsi], ymm4 #6.13 // store to str2
add r8, 32 #4.5 // i+=32
cmp r8, rax #4.5
jb ..B1.15 # Prob 82% #4.5 // }while(i<len);
Для безопасности потоков хорошо известно, что изобретать запись с помощью неатомарного чтения/перезаписи небезопасно.
Абстрактная машина C++ никогда не касается str2
вообще, поэтому недействительны любые аргументы для однострочной версии о невозможности UB для гонки данных, поскольку чтение str
в то время, когда другой поток пишет, что это уже UB. Даже C++ 20 std::atomic_ref
не меняет этого, потому что мы читаем std::atomic_ref
указатель.
Но что еще хуже, str2
может быть nullptr
. Или указание на близкий к концу объект (который хранится ближе к концу страницы), с str1
содержащим символы, такие, что после конца str2
будет. Мы могли бы даже организовать, чтобы только последний последний байт (str2[len-1]
) находился на новой странице, так чтобы он находился за концом допустимого объекта. Это даже законно, чтобы создать такой указатель (пока вы не разыграли). Но было бы законно передать str2=nullptr
; код позади if()
, который не запускается, не вызывает UB.
Или другой поток параллельно выполняет ту же функцию поиска/замены с другим ключом/заменой, который будет записывать только разные элементы str2
. Неатомарная загрузка/хранение неизмененных значений будет переходить на измененные значения из другого потока. В соответствии с моделью памяти C++ 11 это определенно позволило различным потокам одновременно касаться разных элементов одного и того же массива. C++ модель памяти и условия гонки на массивах символов. (Вот почему char
должен быть размером с наименьшую единицу памяти, которую целевая машина может записать без неатомарного RMW. Внутренний атомарный RMW для байтовых хранений в кеш, тем не менее, хорош и не останавливает байтовое хранилище. инструкции от того, чтобы быть полезным.)
(Этот пример допустим только для отдельной версии str1/str2, потому что чтение каждого элемента означает, что потоки будут считывать элементы массива, другой поток может находиться в середине записи, что является гонкой данных UB.)
Как Herb Sutter упоминается в atomic<>
оружии: C++ Модель памяти и современные аппаратные средства Часть 2: (. Включая общие ошибки) Ограничения на компиляторы и аппаратном обеспечении; генерация кода и производительность на x86/x64, IA64, POWER, ARM и др.; расслабленная атомика; изменчивость: отсеивание неатомного кода RMW было постоянной проблемой для компиляторов после стандартизации C++ 11. Мы прошли большую часть пути, но у очень агрессивных и менее распространенных компиляторов, таких как ICC, явно есть ошибки.
(Тем не менее, я уверен, что разработчики компиляторов Intel сочтут это ошибкой.)
Несколько менее правдоподобных (чтобы увидеть в реальной программе) примеров, которые это также сломало бы:
Помимо nullptr
, вы можете передать указатель на (массив) std::atomic<T>
или мьютекс, где неатомарное чтение/перезапись разрушает вещи, изобретая записи. (char*
может иметь псевдоним что угодно).
Или str2
указывает на буфер, который вы вырезали для динамического выделения, и у ранней части str1
будут некоторые совпадения, но более поздние части str1
не будут иметь совпадений, и эта часть str2
используется другими потоками, (И по какой-то причине вы не можете легко рассчитать длину, которая останавливает короткое замыкание).
Для будущих читателей: если вы хотите, чтобы компиляторы автоматически векторизовались таким образом:
Вы можете написать источник как str2[i] = x? replacement: str2[i];
str2[i] = x? replacement: str2[i];
который всегда записывает строку в абстрактную машину C++. IIRC, что позволяет gcc/clang векторизовать то, что делает ICC после небезопасного преобразования if в blend.
Теоретически оптимизирующий компилятор может превратить его обратно в условную ветвь в скалярной очистке или что угодно, чтобы избежать ненужного загрязнения памяти. (Или если нацелен на ISA, такой как ARM32, где возможно предикатное хранилище, вместо только операций выбора ALU, таких как x86 cmov
, PowerPC isel
или AArch64 csel
. Предикатные инструкции ARM32 архитектурно являются NOP, если предикат равен false).
Или, если компилятор x86 выбрал использование хранилищ с масками AVX512, это также позволило бы безопасно векторизовать, как это делает ICC: хранилища с масками выполняют подавление сбоев и никогда не сохраняют элементы, для которых маска ложна. (При использовании регистра маски с загрузкой и запоминанием AVX-512 возникает ли ошибка при недопустимом доступе к замаскированным элементам?).
vpcmpeqb k1, zmm0, [rdi] ; compare from memory into mask
vmovdqu8 [rsi]{k1}, zmm1 ; masked store that only writes elements where the mask is true
ICC19 фактически делает это в основном (но с индексированными режимами адресации) с -march=skylake-avx512
. Но с векторами ymm, потому что 512-битный макс. Турбо снижает макс. Слишком много, чтобы оно того стоило, если ваша программа не использует AVX512 в любом случае на Skylake Xeons.
Поэтому я думаю, что ICC19 безопасен при векторизации этого с AVX512, но не AVX2. Если в коде очистки нет проблем, когда он делает что-то более сложное с vpcmpuq
и kshift
/kor
, kshift
с нулевой маской и маскированным сравнением в другую маску reg.
AVX1 имеет замаскированные хранилища ( vmaskmovps/pd
) с подавлением сбоев и всем остальным, но до AVX512BW нет детализации, более узкой, чем 32 бита. Целочисленные версии AVX2 доступны только с гранулярностью dword/qword, vpmaskmovd/q
.