Ответ 1
Да, это безопасно в x86 asm, и существующие реализации libc strlen(3)
используют это в рукописном asm. И даже glibc fallback C, но он компилируется без LTO, поэтому он никогда не может быть встроенным. Это в основном использование C в качестве переносимого ассемблера для создания машинного кода для одной функции, а не как часть большой программы на C с встраиванием. Но это главным образом потому, что он также имеет потенциальный UB со строгим псевдонимом, см. мой ответ на связанной Q & A. Возможно, вы также захотите использовать GNU C __attribute__((may_alias))
typedef вместо обычного unsigned long
, поскольку ваш более широкий тип, такой как __m128i
и т.д., Уже используется.
Это безопасно, потому что выровненная нагрузка никогда не пересечет более высокую границу выравнивания, и защита памяти происходит с выровненными страницами, поэтому, по крайней мере, 4 тыс. Границ 1 Не может произойти сбой любой естественно выровненной нагрузки, которая касается хотя бы одного действительного байта.
В некоторых случаях может быть полезно просто проверить, что адрес находится достаточно далеко от границы следующей страницы 4k; это также безопасно. например отметьте ((p + 15) ^ p) & 0xFFF...F000 == 0
(LEA/XOR/TEST), который сообщает, что последний байт 16-байтовой загрузки имеет те же биты адреса страницы, что и первый байт. Или p+15 <= p|0xFFF
(LEA/OR/CMP с лучшим ILP) проверяет, что последний байт-адрес загрузки & lt; = последний байт страницы, содержащей первый байт.
Насколько я знаю, он также безопасен на C, скомпилированном для x86. Чтение вне объекта, конечно, является неопределенным поведением в C, но работает в C-target-x86. Я не думаю, что компиляторы явно/специально определяют поведение, но на практике это работает таким образом.
Я думаю, что не тот тип UB, который, как предполагают агрессивные компиляторы, не может произойти при оптимизации, но подтверждение от автора-компилятора по этому вопросу было бы хорошо, особенно в тех случаях, когда это легко доказать при компиляции. время, когда доступ выходит за пределы конца объекта. (См. обсуждение в комментариях с @RossRidge: в предыдущей версии этого ответа утверждалось, что это было абсолютно безопасно, но что сообщение в блоге LLVM на самом деле не читается так).
Это требуется в asm, чтобы быстрее, чем 1 байт, обрабатывать строку неявной длины. В C теоретически компилятор может знать, как оптимизировать такой цикл, но на практике это не так, поэтому приходится делать подобные хаки. До тех пор, пока это не изменится, я подозреваю, что люди, заботящиеся о компиляторах, обычно избегают взлома кода, содержащего этот потенциальный UB.
Там нет никакой опасности, когда перечитывание не видно для кода, который знает, как долго объект. Компилятор должен создать asm, который работает для случая, когда есть элементы массива, насколько мы на самом деле читаем. Вероятная опасность, которую я вижу с возможными будущими компиляторами, заключается в следующем: после встраивания компилятор может увидеть UB и решить, что этот путь выполнения никогда не следует выбирать. Или, что условие завершения должно быть найдено перед последним неполным вектором и пропустить его при полной развертке.
Получаемые вами данные являются непредсказуемым мусором, но других побочных эффектов не будет. Пока ваша программа не зависит от байтов мусора, это нормально. (например, используйте битхэки, чтобы определить, равен ли один из байтов uint64_t
нулю, затем байтовый цикл, чтобы найти первый нулевой байт, независимо от того, какой мусор находится за его пределами.)
Необычные ситуации, когда это не безопасно в x86 asm
Точки останова аппаратных данных (точки наблюдения), которые запускаются при загрузке с заданного адреса. Если есть переменная, которую вы отслеживаете сразу после массива, вы можете получить ложный удар. Это может быть небольшим раздражением для того, кто отлаживает обычную программу. Если ваша функция будет частью программы, которая использует отладочные регистры x86 D0-D3 и возникающие исключения для чего-то, что может повлиять на корректность, то будьте осторожны с этим.
В гипотетической 16 или 32-битной ОС, которая использует сегментацию: ограничение сегмента может использовать 4-килобайтную или 1-байтовую гранулярность, поэтому можно создать сегмент с первым ошибочным смещением странный. (Выравнивание базы сегмента по строке или странице кэша не имеет значения, за исключением производительности). Все основные операционные системы x86 используют модели с плоской памятью, а x86-64 убирает поддержку ограничений сегментов для 64-битного режима.
Отображаемые в память регистры ввода/вывода сразу после буфера, который вы хотели зациклить при больших нагрузках, особенно с той же строкой кэша 64B. Это крайне маловероятно, даже если вы вызываете такие функции из драйвера устройства (или из программы пользовательского пространства, например, X-сервера, который отображает некоторое пространство MMIO).
Если вы обрабатываете 60-байтовый буфер и вам нужно избегать чтения из 4-байтового регистра MMIO, вы будете знать об этом и будете использовать
volatile T*
. Такая ситуация не бывает для нормального кода.
strlen
является каноническим примером цикла, который обрабатывает буфер неявной длины и, таким образом, не может векторизовать, не считывая после конца буфера. Если вам нужно избежать чтения после завершающего байта 0
, вы можете читать только один байт за раз.
Например, реализация glibc использует пролог для обработки данных вплоть до первой границы выравнивания 64B. Затем в основном цикле (ссылка gitweb на источник asm) загружает целую строку кэша 64B, используя четыре выравниваемых загрузки SSE2. Он объединяет их в один вектор с pminub
(мин. Байтов без знака), поэтому конечный вектор будет иметь нулевой элемент, только если любой из четырех векторов имеет ноль. Обнаружив, что конец строки находится где-то в этой строке кэша, он перепроверяет каждый из четырех векторов отдельно, чтобы увидеть, где. (Использование типичного pcmpeqb
для вектора со всеми нулями и pmovmskb
/bsf
для определения положения в векторе.) Glibc имел обыкновение иметь пару различные стратегии strlen на выбор, но текущая подходит для всех процессоров x86-64.
Обычно такие циклы избегают касания каких-либо дополнительных строк кэша, которые им не нужны, а не только страниц, по соображениям производительности, например, glibc strlen.
Загрузка 64B за раз, конечно, безопасна только из указателя, выровненного по 64B, так как доступ с естественным выравниванием не может пересекать границы строки кэша или строки страницы.
Если вы заранее знаете длину буфера, вы можете избежать чтения за концом, обрабатывая байты за пределами последнего полностью выровненного вектора, используя невыровненную загрузку, которая заканчивается на последнем байте буфера.
(Опять же, это работает только с идемпотентными алгоритмами, такими как memcpy, которым все равно, перекрывают ли они хранилища в месте назначения. Алгоритмы модификации на месте часто не могут этого сделать, кроме как с помощью чего-то вроде преобразования строки в верхний регистр с SSE2, где это нормально для обработки данных, которые уже были переданы в регистр. Кроме остановки пересылки хранилища, если вы выполняете невыровненную загрузку, которая перекрывается с вашим последним выровненным хранилищем.)
Поэтому, если вы векторизуете буфер известной длины, часто лучше в любом случае избегать избыточного.
Безошибочное перечитывание объекта - это вид UB, который определенно не может повредить, если компилятор не может увидеть его во время компиляции. Полученный asm будет работать так, как если бы дополнительные байты были частью какого-то объекта.
Но даже если он виден во время компиляции, он обычно не мешает текущим компиляторам.
PS: в предыдущей версии этого ответа утверждалось, что не выровненное значение int *
было также безопасно в C, скомпилированном для x86. Это не так. Я был слишком кавалером 3 года назад, когда писал эту часть. Вам нужен typedef __attribute__((aligned(1)))
или memcpy
, чтобы сделать это безопасным.
Набор вещей, который ISO C оставляет неопределенным, но то, что для встроенных функций Intel требуются компиляторы, определяет создание не выровненных указателей (по крайней мере, с такими типами, как __m128i*
), но не разыменовывает их напрямую. Является ли reinterpret_cast между аппаратным указателем вектора и соответствующим типом неопределенным поведением?