Насколько я могу судить, ваша программа правильно сформирована и не имеет неопределенного поведения.Абстрактная машина C ++ фактически никогда не присваивает объекту const
. Недостаточно if()
достаточно, чтобы «спрятать» / «защитить» вещи, которые были бы UB, если бы они выполнялись. Единственное, от чего if(false)
не может вас спасти, это неправильно сформированная программа, например, синтаксические ошибки или попытка использовать расширения, которые не существуют в этом компиляторе или целевой арке.
Компиляторам вообще не разрешено изобретать записи с if-преобразование в код без ответвлений.
Удаление const
допустимо, если вы фактически не назначаете его.например, для передачи указателя на функцию, которая не является константно-правильной, и принимает вход только для чтения с указателем не const
.Ответ, который вы указали на Разрешено ли отбрасывать const на объекте, заданном const, если он фактически не изменен? является правильным.
Поведение ICCВот не свидетельство UB в ISO C ++ или C. Я думаю, что ваши рассуждения являются правильными, и это хорошо определено.Вы нашли ошибку ICC.Если кому-то все равно, сообщите об этом на своих форумах: https://software.intel.com/en-us/forums/intel-c-compiler. Существующие сообщения об ошибках в этом разделе их форума были приняты разработчиками, например, этот .
Мы можем построить пример , где он автоматически векторизуется таким же образом (с безусловным и неатомарным чтением / возможно-изменением / перезаписью) , где он явно недопустимо, потому что чтение / перезапись происходит на 2-й строке, которую абстрактная машина C даже не читает.
Таким образом, мы не можем доверять генератору кода ICC, чтобы сказать нам что-нибудьо том, когда мы вызвали UB, потому что он сделает код сбоя даже в явно законных случаях.
Godbolt : ICC19.0.1 -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
не меняет этого, потому что мы читаем через неатомарный указатель.
Но, что еще хуже, str2
может быть nullptr
. или указывать на близкий к концу объект (который хранится ближе к концу страницы), с str1
, содержащим символы, такие, что после конца * 1072 нет записей./ страница будет происходить.Мы могли бы даже организовать, чтобы только последний байт (str2[len-1]
) был на новой странице, так что это один за другим конец действительного объекта.Это даже законно, чтобы создать такой указатель (пока вы не разыграете).Но было бы законно передать str2=nullptr
;код за if()
, который не запускается, не вызывает UB.
Или другой поток параллельно выполняет ту же функцию поиска / замены с другим ключом / заменой, который будет только писатьразличные элементы str2
. Неатомарная загрузка / сохранение неизмененных значений будет переходить на измененные значения из другого потока.Согласно модели памяти C ++ 11, определенно разрешено, чтобы разные потоки одновременно касались разных элементов одного и того же массива. Модель памяти C ++ и условия гонки на массивах символов .(Вот почему char
должен быть размером с наименьшую единицу памяти, которую целевая машина может записать без неатомарного RMW. Внутренний атомарный RMW для байтовых хранилищ в кеше вполне подойдет , однако,и не мешает использованию инструкций в хранилище байтов.)
(Этот пример допустим только для отдельной версии str1 / str2, поскольку чтение каждого элемента означает, что потоки будут считывать элементы массива, другой поток может находиться в середине записи, что является гонкой данных UB.)
Как упоминал Херб Саттер в atomic<>
Оружие: модель памяти C ++ и современное оборудование Часть 2: Ограничения на компиляторы и аппаратные средства (включая общие ошибки) ;генерация кода и производительность на x86 / x64, IA64, POWER, ARM и др .;расслабленная атомика;volatile : отсеивание неатомного кода RMW было постоянной проблемой для компиляторов после стандартизации C ++ 11.Мы прошли большую часть пути, но у очень агрессивных и менее распространенных компиляторов, таких как ICC, явно есть ошибки.
(Тем не менее, я уверен, что разработчики Intel для компиляторов будут Считайте это ошибкой.)
Несколько менее правдоподобных (чтобы увидеть в реальной программе) примеров, которые это также может сломать:
Помимо nullptr
, вы можете передать указательto (массив) std::atomic<T>
или мьютекс, где неатомарное чтение / перезапись ломает вещи, изобретая записи.(char*
может иметь псевдоним что угодно ).
или str2
указывает на буфер, который вы вырезали для динамического выделения, и ранняя часть str1
будет иметь некоторыесовпадения, но более поздние части str1
не будут иметь совпадений, и эта часть str2
используется другими потоками.(И по какой-то причине вы не можете легко рассчитать длину, которая останавливает короткое замыкание).
Для будущих читателей: если вы хотите, чтобы компиляторы автоматически векторизовались таким образом:
Вы можете написать источник, например 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
, загрузкой с нулевой маской и маскированным сравнением в другую маску, рег.
AVX1 имеет замаскированные хранилища (vmaskmovps/pd
) с подавлением отказов и всем, но до AVX512BW нет детализации, более узкой, чем 32 бита.Целочисленные версии AVX2 доступны только с гранулярностью dword / qword, vpmaskmovd/q
.