Сбой с icc: может ли компилятор изобрести записи там, где их не было в абстрактной машине?

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

#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_repl1. Например, 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-подобным" кодом, но я хотел максимально упростить анализ и сделать его полностью автономным.

Хм, я не вижу здесь никакого УБ. Надеюсь, ответы на комментарии станут реальными ответами...

Baum mit Augen 04.02.2019 23:06

Но... строковый литерал изменен нет, потому что он не содержит символа /, а все модификации основаны на наличии символа /. Это действительно включает интерпретацию «на самом деле никогда не модифицировалась». Оптимизатор полагает, что выполнять операцию логический для строки безопасно, но на самом деле в данном случае это не так. Увлекательный вопрос; Мне не терпится увидеть, что скажут ответы.

Cody Gray 04.02.2019 23:07

«Является ли мой код законным C++» — конечно, допустимым в том смысле, что вы можете иметь код, который устанавливает указатель в NULL, а затем обрабатывает его как массив. Допустимо для компиляции, не определено для выполнения. C++ позволяет вам передать const в неконстантную функцию, или преобразовать int в число с плавающей запятой, или использовать все виды других действий, приводящих к разрушению стека и генерации исключений. Вы предполагаете, что компилятор достаточно умен, чтобы понять, что он ничего не может сделать в вашей неконстантной функции, но это не гарантируется. Это не обещание языка спасти вас здесь от самих себя.

Dave S 04.02.2019 23:10

@DaveS Как это UB? Пожалуйста, ответьте в разделе ответов, чтобы мы могли правильно проголосовать за ваш пост.

Baum mit Augen 04.02.2019 23:12

Я утверждаю, что это UB, потому что вы передаете константную строку неконстантной функции, которая включает оператор присваивания. Мы, люди, знаем, что нет необходимости выполнять оптимизированный код для функции, компилятор не был достаточно умен, чтобы понять это.

Dave S 04.02.2019 23:16

Разве это не тот же вопрос, на который вы ссылаетесь (например, Вот этот)? То же самое верно - у вас есть объявленный объект const, который мы передаем в дескриптор функции, которая мог бы изменяет его, но на самом деле не изменяет?

Barry 04.02.2019 23:17

@DaveS Ваше утверждение о том, что простое присутствие присваивания в пути мертвого кода уже вызывает UB, нуждается в обосновании.

Baum mit Augen 04.02.2019 23:17

Я думаю, что вопрос шире - независимо от константности, разрешено ли компилятору записывать в память, когда запись не ожидается?

rustyx 04.02.2019 23:22

@rustyx: В общем, изобретение записи (неатомарная загрузка и последующее сохранение того же значения) является огромным запретом для компиляторов. Но это более очевидная проблема, когда C++ читает один массив и условно записывает в другой. Если источник C++ читает каждый символ, это будет UB для другого потока, который будет писать его одновременно. Пока эта загрузка/blendv/store не выходит за пределы указателя+длины (и неатомарно считывает/переписывает некоторую память, которая может быть блокировкой или чем-то еще), это может быть законным из памяти С++ 11 модель потокобезопасности POV.

Peter Cordes 04.02.2019 23:45

@DaveS - чтобы быть очень явным, когда я говорю «законно», я имею в виду компилировать и выполнять с дополнительным аргументом и без него. Я думаю, что это подразумевает вопрос, который в основном говорит о поведении во время выполнения, но в любом случае есть ваше разъяснение.

BeeOnRope 05.02.2019 00:07

@Peter, у меня была такая же мысль, поэтому я сформировал этот пример вокруг константной строки и раздела .rodata, а не аргумента о безопасности потоков. У него также есть то преимущество, что проблема очевидна: он всегда дает сбой, а не более абстрактная проблема, связанная с условиями гонки, которую может быть трудно проиллюстрировать. Тем не менее, можно найти способ изменить пример таким образом, чтобы возникла проблема с моделью памяти.

BeeOnRope 05.02.2019 00:11

Повторяя Барри, разве вы не должны просто отправить отчет об ошибке после вашего предыдущего вопроса?

Passer By 05.02.2019 02:02

@PasserBy - хорошо, когда я писал этот другой вопрос, я не знал о поведении icc: меня интересовал именно вопрос const_cast (хотя оказывается, что вопрос тот может быть дубликатом). Сегодня кто-то указал мне, что icc на самом деле векторизует этот тип кода, и поэтому я создал этот пример, который дает сбой: теперь мой вопрос касается этого конкретного кода, валидности оптимизации и допустимости сбоя. Да, аспект const_cast является его частью, поэтому я связал другой вопрос (чтобы предотвратить все «вы не можете сделать это с постоянными комментариями»).

BeeOnRope 05.02.2019 04:20

Речь идет о поведении icc именно в этом сценарии: может быть, есть какая-то другая причина, по которой разрешена их оптимизация? Насчет регистрации ошибки, это вообще возможно? icc — это коммерческий продукт с закрытым исходным кодом, и у меня нет контракта на поддержку. Не похоже, что существует какой-либо очевидный способ для публики, чтобы сообщать об ошибках. В любом случае, у меня ограниченный интерес к icc: это было больше похоже на «о, может ли компилятор сделать это, или это плохо?» тип вещи.

BeeOnRope 05.02.2019 04:22

@PeterCordes — даже если массивы различны, icc по-прежнему пишет в целевой массив. Это кажется совершенно неправильным не только с точки зрения модели памяти, но и того, что я передаю в nullptr для второго или массива, или более короткого массива, или чего-то еще? Просто кажется, что эта векторизация на основе смешивания нарушена.

BeeOnRope 05.02.2019 04:53

Хорошо, да, это просто 100% сломано. Нелегко придумать случай, когда это ломает что-то, что кто-то на самом деле мог написать в программе, предназначенной для использования, но очень легко придумать сценарии, в которых вы можете законно запустить эту функцию со строкой str1, которая никогда не совпадает, и ICC ломает вещи. Помимо nullptr, вы можете передать указатель на std::atomic<T> или мьютекс, где неатомарное чтение/перезапись ломает вещи, изобретая записи. re: отчет: Google для отчета об ошибке ICC находит их форум, например: software.intel.com/en-us/forums/intel-c-compiler/topic/75376‌​7

Peter Cordes 05.02.2019 05:25

Для будущих читателей: если вы хотите, чтобы компиляторы автоматически векторизовали таким образом, вы можете написать исходный код, например str2[i] = x ? replacement : str2[i];, который всегда записывает строку. Теоретически оптимизирующий компилятор может превратить его в условную ветвь в скалярной очистке или что-то еще, чтобы избежать ненужного загрязнения памяти. (Или, если вы нацелены на ISA, например ARM32, где возможно предварительное хранилище, а не только операции выбора ALU. Или x86 с маскированными хранилищами AVX512, где это действительно было бы будет безопасным.)

Peter Cordes 05.02.2019 05:30

Intel слишком любит спекулировать.

Language Lawyer 05.02.2019 05:50

@Peter Действительно, и gcc, и clang векторизуют циклы, если вы пишете их с безусловным хранилищем с условными данными. Однако я видел много других случаев, когда они не векторизуются, даже если вы пытаетесь научить их, что доступ к памяти осуществляется, поэтому это не всегда работает. AVX2 имеет маскированные загрузки и сохранения, но только с гранулярностью 4 и 8 байт (VPMASKMOV).

BeeOnRope 05.02.2019 06:28

что-то изменится, если вы используете статическую или встроенную функцию?

phön 05.02.2019 09:40

@phön - на самом деле в моем примере функция встроена. Фрагмент сборки, который я показываю, взят из встроенного тела внутри main(), а не из отдельной бесплатной функции. Материал с argv необходим из-за этого встраивания, иначе компилятор полностью опускает код замены.

BeeOnRope 05.02.2019 13:05

Связанный вопрос: Что делает компилятор C++, чтобы обеспечить безопасное использование разных, но смежных участков памяти в разных потоках? - см., в частности, мой ответ на него, который частично касается, как вы выразились, «изобретенной записи». (Обратите внимание, что в этом случае, однако, компилятор знает, что c не может быть const_cast-объектом const, и, следовательно, некоторые оптимизации возможны в однопоточном MM.)

Arne Vogel 05.02.2019 13:24
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
31
22
758
0

Другие вопросы по теме