Многие методы, которые можно найти в высокопроизводительных алгоритмах, можно (и) упростить, если им разрешено считывать небольшое количество после окончания входных буферов. Здесь "небольшое количество" обычно означает до W - 1
байтов после конца, где W
- размер слова в байтах алгоритма (например, до 7 байтов для алгоритма, обрабатывающего ввод в 64-битных блоках).
Ясно, что запись после конца входного буфера в общем случае небезопасна, так как вы можете забить данные за пределы буфера 1. Также ясно, что чтение после окончания буфера на другую страницу может вызвать ошибку сегментации/нарушение доступа, поскольку следующая страница может быть недоступна для чтения.
Однако в особом случае чтения выровненных значений сбой страницы кажется невозможным, по крайней мере, в x86. На этой платформе страницы (и, следовательно, флаги защиты памяти) имеют гранулярность 4 КБ (возможны большие страницы, например, 2 МБ или 1 ГБ, но они кратны 4 КБ), поэтому выровненные операции чтения будут иметь доступ только к байтам на той же странице, что и действительные часть буфера.
Вот канонический пример некоторого цикла, который выравнивает свои входные данные и читает до 7 байтов после конца буфера:
int processBytes(uint8_t *input, size_t size) {
uint64_t *input64 = (uint64_t *)input, end64 = (uint64_t *)(input + size);
int res;
if (size < 8) {
// special case for short inputs that we aren't concerned with here
return shortMethod();
}
// check the first 8 bytes
if ((res = match(*input)) >= 0) {
return input + res;
}
// align pointer to the next 8-byte boundary
input64 = (ptrdiff_t)(input64 + 1) & ~0x7;
for (; input64 < end64; input64++) {
if ((res = match(*input64)) > 0) {
return input + res < input + size ? input + res : -1;
}
}
return -1;
}
Внутренняя функция int match(uint64_t bytes)
не показана, но это то, что ищет байт, соответствующий определенному шаблону, и возвращает наименьшую такую позицию (0-7), если найдена, или -1 в противном случае.
Во-первых, случаи с размером & lt; 8 закладываются в другую функцию для простоты изложения. Затем выполняется одна проверка для первых 8 (не выровненных байтов). Затем выполняется цикл для оставшихся фрагментов floor((size - 7) / 8)
по 8 байт 2. Этот цикл может считывать до 7 байтов после конца буфера (7-байтовый случай возникает, когда input & 0xF == 1
). Однако обратный вызов имеет проверку, которая исключает любые ложные совпадения, которые происходят за пределами буфера.
Практически говоря, безопасна ли такая функция на x86 и x86-64?
Эти типы переопределений распространены в высокопроизводительном коде. Специальный хвостовой код, чтобы избежать таких перегибов, также распространен. Иногда вы видите, как последний тип заменяет первый, чтобы заставить замолчать такие инструменты, как valgrind. Иногда вы видите предложение сделать такую замену, которое отклоняется на том основании, что идиома безопасна, а инструмент ошибочен (или просто слишком консервативен) 3.
Примечание для языковых юристов:
Чтение из указателя сверх его выделенного размера определенно не допускается в стандарте. Я ценю языковые ответы адвоката и даже иногда пишу их сам, и я даже буду счастлив, когда кто-то раскопает главу и стих, который показывает код выше, является неопределенным поведением и, следовательно, небезопасно в самом строгом смысле (и я скопирую детали здесь). В конечном счете, это не то, что Я после. На практике, многие распространенные идиомы включают указатель преобразование, структура доступа, хотя такие указатели и так технически не определены, но широко распространены в высоком качестве и код производительности. Часто нет альтернативы или альтернативы работает на половине скорости или меньше.
Если вы хотите, рассмотрите измененную версию этого вопроса, а именно:
После того, как приведенный выше код был скомпилирован в сборку x86/x86-64, и пользователь убедился, что он скомпилирован ожидаемым образом (т.е. компилятор не использовал доказуемый частично за пределами доступа к сделать что-то действительно умный, безопасное выполнение скомпилированной программы?
В этом отношении этот вопрос является и вопросом C, и вопросом сборки x86. Большая часть кода, использующего этот трюк, который я видел, написана на C, и C по-прежнему является доминирующим языком для высокопроизводительных библиотек, легко затмевая низкоуровневые вещи, такие как asm, и высокоуровневые вещи, такие как & lt; все остальное & gt;. По крайней мере, за пределами жесткой цифровой ниши, где Фортран все еще играет в мяч. Поэтому меня интересует представление вопроса на языке C-compiler-and-under, поэтому я не сформулировал его как вопрос о чистой сборке x86.
Все это говорит, пока я лишь умеренно заинтересован в ссылке на стандарт, показывающий это UD, я очень заинтересован в любых деталях фактические реализации, которые могут использовать этот конкретный UD для производства неожиданный код Я не думаю, что это может произойти без довольно глубокий анализ кросс-процедур, но gcc переполнен тоже удивило многих...
1 Даже в явно безвредных случаях, например, когда записывается одно и то же значение, оно может нарушить параллельный код.
2 Примечание, чтобы это перекрытие работало, требуется, чтобы эта функция и функция match()
вели себя определенным идемпотентным образом - в частности, чтобы возвращаемое значение поддерживало проверки с перекрытием. Таким образом, работает "поиск шаблона соответствия первого байта", поскольку все вызовы match()
все еще в порядке. Однако метод "счетчик байтов соответствует шаблону" не сработает, поскольку некоторые байты могут быть подсчитаны дважды. Кроме того: некоторые функции, такие как вызов "вернуть минимальный байт", будут работать даже без ограничения по порядку, но должны проверять все байты.
3 Стоит отметить, что для valgrind Memcheck существует флаг, --partial-loads-ok
, который определяет, действительно ли такие чтения сообщаются как ошибка. По умолчанию да, это означает, что в целом такие загрузки не рассматриваются как непосредственные ошибки, но что предпринимаются усилия для отслеживания последующего использования загруженных байтов, некоторые из которых являются действительными, а некоторые нет, с пометкой ошибки. если используются байты вне диапазона. В таких случаях, как в примере выше, в котором доступ ко всему слову осуществляется в match()
, такой анализ позволит сделать вывод, что байты доступны, даже если результаты в конечном итоге отбрасываются. Valgrind в общем не может определить, действительно ли используются недопустимые байты из частичной загрузки (и обнаружение в целом, вероятно, очень сложно).