Насколько я могу судить, ваша программа хорошо сформирована и не имеет неопределенного поведения. Абстрактная машина C ++ никогда не назначает объект const
. Невыбранного if()
достаточно, чтобы "спрятать" / "защитить" вещи, которые были бы UB, если бы они выполнялись. Единственное, от чего if(false)
не может вас спасти, - это плохо сформированная программа, например синтаксические ошибки или попытки использовать расширения, которых нет в этом компиляторе или целевой архитектуре.
Обычно компиляторам не разрешается изобретать операции записи с преобразованием if в автономный код.
Отбрасывание const
является законным, если вы на самом деле не назначаете его через него. например для передачи указателя на функцию, которая не является корректной по константе и принимает входные данные только для чтения с указателем, отличным от const
. Ответ, который вы указали на Разрешено ли отбрасывать константу для объекта, определенного константой, если он фактически не изменен? правильно.
Поведение ICC здесь не свидетельствует о наличии UB в ISO C ++ или C. Я думаю, что ваши доводы правильны, и они четко определены. Вы обнаружили ошибку ICC. Если кому-то интересно, сообщите об этом на их форумах: https://software.intel.com/en-us/forums/intel-c-compiler. Существующие отчеты об ошибках в этом разделе своего форума были приняты разработчиками, например этот.
Мы можем построить пример, в котором он автоматически векторизуется таким же образом (с безусловным и неатомарным чтением / возможно-изменением / перезаписью) где он четко незаконно, потому что чтение / перезапись происходит во второй строке, которую абстрактная машина 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
, содержащим символы, которые не записываются после конца str2
/ страница произойдет. Мы могли бы даже сделать так, чтобы на новой странице находился только самый последний байт (str2[len-1]
), чтобы он был на один конец действительного объекта. Создание такого указателя даже законно (если вы не deref). Но было бы законно передать str2=nullptr
; код за if()
, который не запускается, не вызывает UB.
Или другой поток выполняет ту же функцию поиска / замены параллельно с другим ключом / заменой, который будет записывать только разные элементы str2
. Неатомарная загрузка / сохранение неизмененных значений будет происходить после изменения значения из другого потока. В соответствии с моделью памяти C ++ 11 определенно разрешено, чтобы разные потоки одновременно касались разных элементов одного и того же массива. Модель памяти C ++ и условия гонки для массивов char. (Вот почему размер char
должен быть равен наименьшей единице памяти, которую целевая машина может записывать без неатомарного RMW. внутренний атомарный RMW для хранения байтов в кеше, тем не менее, прекрасен и не останавливает выполнение инструкций по хранению байтов быть полезным.)
(Этот пример допустим только с отдельной версией str1 / str2, потому что чтение каждого элемента означает, что потоки будут читать элементы массива, а другой поток может быть в середине записи, что является UB гонки данных.)
Как упоминал Херб Саттер в atomic<>
Оружие: модель памяти C ++ и современное оборудование Часть 2: Ограничения на компиляторы и оборудование (включая распространенные ошибки); генерация кода и производительность на x86 / x64, IA64, POWER, ARM и др .; расслабленная атомика; volatile: устранение неатомарного генератора кода RMW было постоянной проблемой для компиляторов после стандартизации C ++ 11. Мы почти достигли цели, но в очень агрессивных и менее распространенных компиляторах, таких как ICC, явно все еще есть ошибки.
(Однако я почти уверен, что разработчики компиляторов Intel сочтут это ошибкой.)
Некоторые менее правдоподобные (чтобы увидеть в реальной программе) примеры, которые также могут сломаться:
Помимо nullptr
, вы можете передать указатель на (массив) 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, если предикат ложен).
Или, если компилятор 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-битное значение max turbo слишком сильно, чтобы того стоить, если вся ваша программа не использует AVX512, в любом случае на Skylake Xeon.
Поэтому я думаю, что ICC19 безопасен при векторизации с помощью AVX512, но не AVX2. Если нет проблем в его коде очистки, где он делает что-то более сложное с vpcmpuq
и kshift
/ kor
, загрузкой с нулевой маской и сравнением с маской с другим регистром маски.
В AVX1 есть замаскированные магазины (vmaskmovps/pd
) с подавлением ошибок и всем остальным, но до AVX512BW нет более узкой детализации чем 32 бита. Целочисленные версии AVX2 доступны только с детализацией dword / qword, vpmaskmovd/q
.
person
Peter Cordes
schedule
05.02.2019
/
, и все изменения основаны на наличии символа/
. Это действительно включает интерпретацию того, что фактически никогда не изменялось. Оптимизатор предполагает, что выполнение логической операции над строкой безопасно, но на самом деле в данном случае это не так. Увлекательный вопрос; Я очень хочу увидеть, что скажут ответы. - person Cody Gray   schedule 05.02.2019const
, который мы передаем дескриптору функции, которая может изменить его, но на самом деле не делает? - person Barry   schedule 05.02.2019const_cast
(оказывается, этот вопрос может быть дубликатом). Сегодня кто-то указал мне, что icc на самом деле векторизует этот тип кода, и поэтому я создал этот пример, который дает сбой: теперь мой вопрос касается этого конкретного кода и валидности оптимизации и допустимости сбоя. Да, аспектconst_cast
является его частью, поэтому я связал другой вопрос (чтобы предотвратить все, что вы не можете сделать с помощью комментариев const). - person BeeOnRope   schedule 05.02.2019nullptr
для второго или массива, или более короткого массива или чего-то еще? Просто похоже, что эта векторизация на основе смешивания сломана. - person BeeOnRope   schedule 05.02.2019nullptr
, вы можете передать указатель наstd::atomic<T>
или мьютекс, где неатомарное чтение / перезапись ломает вещи, изобретая записи. re: Reporting: отчет об ошибке google для ICC находит их форум, например: software.intel.com/en-us/forums/intel-c-compiler/topic/753767 - person Peter Cordes   schedule 05.02.2019str2[i] = x ? replacement : str2[i];
, который всегда записывает строку. Теоретически оптимизирующий компилятор может превратить его в условную ветвь в скалярной очистке или что-то еще, чтобы избежать ненужного загрязнения памяти. (Или если нацелена на ISA, например ARM32, где возможно предикатное хранилище, а не только операции выбора ALU. Или x86 с замаскированными хранилищами AVX512, где это действительно было бы безопасно.) - person Peter Cordes   schedule 05.02.2019c
не может бытьconst_cast
edconst
объектом, и поэтому возможны некоторые оптимизации в однопоточном MM.) - person Arne Vogel   schedule 05.02.2019