Сбой с icc: может ли придумать компилятор, где ничего не было в абстрактной машине? - PullRequest
0 голосов
/ 05 февраля 2019

Рассмотрим следующую простую программу:

#include <cstring>
#include <cstdio>
#include <cstdlib>

void replace(char *str, size_t len) {
    for (size_t i = 0; i < len; i++) {
        if (str[i] == '/') {
            str[i] = '_';
        }
    }
}

const char *global_str = "the quick brown fox jumps over the lazy dog";

int main(int argc, char **argv) {
  const char *str = argc > 1 ? argv[1] : global_str;
  replace(const_cast<char *>(str), std::strlen(str));
  puts(str);
  return EXIT_SUCCESS;
}

Она берет (необязательную) строку в командной строке и печатает ее, с / символами, замененными на _.Эта функциональность замены реализована функцией c_repl 1 .Например, a.out foo/bar печатает:

foo_bar

Элементарный материал до сих пор, верно?

Если вы не укажете строку, она удобно использует глобальную строку быстрый коричневый лис перепрыгивает через ленивую собаку , которая не содержит символов /, и такне подвергается какой-либо замене.

Конечно, строковые константы const char[], поэтому мне нужно сначала отбросить константу - вот видите, const_cast.Поскольку строка фактически никогда не изменяется, у меня сложилось впечатление, что это допустимо .

gcc и clang компилирует двоичный файл с ожидаемым поведением, с передачей или без передачи строки в команделиния.ICC аварийно завершает работу, когда вы не предоставляете строку, однако:

icc -xcore-avx2 char_replace.cpp && ./a.out
Segmentation fault (core dumped)

Основной причиной является основной цикл для c_repl, который выглядит следующим образом:

  400c0c:       vmovdqu ymm2,YMMWORD PTR [rsi]
  400c10:       add    rbx,0x20
  400c14:       vpcmpeqb ymm3,ymm0,ymm2
  400c18:       vpblendvb ymm4,ymm2,ymm1,ymm3
  400c1e:       vmovdqu YMMWORD PTR [rsi],ymm4
  400c22:       add    rsi,0x20
  400c26:       cmp    rbx,rcx
  400c29:       jb     400c0c <main+0xfc>

Этовекторизованный цикл.Основная идея состоит в том, что 32 байта загружаются, а затем сравниваются с символом /, формируя значение маски с байтом, установленным для каждого совпадающего байта, а затем существующая строка смешивается с вектором, содержащим 32 _ символа.эффективно заменяет только символы /.Наконец, обновленный регистр записывается обратно в строку с инструкцией vmovdqu YMMWORD PTR [rsi],ymm4.

Это окончательное хранилище дает сбой, потому что строка доступна только для чтения и размещается в разделе .rodata двоичного файла, которыйзагружается с использованием страниц только для чтения.Конечно, хранилище представляло собой логическое «нет операции», записывавшее те же символы, которые оно прочитало, но процессору все равно!

Является ли мой код допустимым C ++, и поэтому я должен обвинить icc в неправильной компиляции,или я куда-то пробираюсь в болото UB?


1 Тот же сбой из той же проблемы происходит с std::replace на std::string, а не на моем "C-подобном""код, но я хотел максимально упростить анализ и сделать его полностью автономным.

1 Ответ

0 голосов
/ 05 февраля 2019

Насколько я могу судить, ваша программа правильно сформирована и не имеет неопределенного поведения.Абстрактная машина 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.

...