SSE: выровненная загрузка и сохранение, пересекающие границу страницы
Я где-то читал, что перед выполнением выровненной загрузки или сохранения рядом с границей страницы (например, используя _mm_loadu_si128
/ _mm_storeu_si128
intrinsics), код должен сначала проверить, принадлежит ли весь вектор (в данном случае 16 байтов) одной и той же странице, и переключиться на не-векторные инструкции, если нет. Я понимаю, что это необходимо для предотвращения coredump, если следующая страница не принадлежит процессу.
Но что, если обе страницы принадлежат процессу (например, они являются частью одного буфера, и я знаю размер этого буфера)? Я написал небольшую тестовую программу, которая выполняла загрузку и сохранение без выравнивания, которые пересекали границу страницы, и она не вылетала. Должен ли я всегда проверять границы страницы в таком случае, или этого достаточно, чтобы не переполнить буфер?
Конверт: Linux, x86_64, gcc
1 ответ
Разделение строк страницы ухудшает производительность, но не влияет на правильность невыровненного доступа. Достаточно убедиться, что вы не читаете за пределами буфера, когда знаете заранее длину.
Для правильности, вам часто нужно беспокоиться об этом при реализации чего-то вроде strlen
где ваш цикл останавливается, когда вы находите дозорное значение. Это значение может быть в любой позиции в вашем векторе, поэтому простое выполнение невыровненных нагрузок в 16B будет считывать конец массива. Если прекращается 0
находится в последнем байте одной страницы, и следующая страница не читается, а указатель текущей позиции не выровнен, загрузка, которая включает 0
Байт также будет включать байты с нечитаемой страницы, поэтому он будет неисправен.
Одним из решений является скаляр до тех пор, пока ваш указатель не будет выровнен, а затем загрузить выровненные векторы. Выровненная загрузка всегда целиком исходит с одной страницы, а также с одной строки кэша. Таким образом, даже если вы прочитаете несколько байтов за концом строки, вы гарантированно не ошибетесь. Valgrind может быть недоволен, но стандартная библиотека strlen
реализации используют это.
Вместо скаляра до выровненного указателя, вы можете сделать невыровненный вектор с начала строки (до тех пор, пока он не пересекает строку страницы), а затем выполнить выравниваемые загрузки. Первая выровненная загрузка будет перекрывать первую не выровненную загрузку, но это совершенно нормально для такой функции, как strlen, которой все равно, если она увидит одни и те же данные дважды.
Возможно, стоит избегать разбиения строк на странице по соображениям производительности. Даже если вы знаете, что ваш указатель src не выровнен, зачастую аппаратному обеспечению легче обрабатывать разбиения строки кэша. Но до Skylake разделение страниц имеет дополнительную задержку ~100c. ( До 5с в Скайлэйке). Если у вас есть несколько указателей, которые могут быть выровнены по-разному относительно друг друга, вы не всегда можете просто использовать пролог для выравнивания вашего src. (например c[i] = a[i] + b[i]
, а также c
выровнен, но b
нет.)
В этом случае, возможно, стоит использовать ветвь для выравнивания нагрузки до и после разбиения страницы и комбинировать их с palignr
,
Ошибочный прогноз ветки (~15c) дешевле, чем задержка разбиения страницы, но затягивает все (не только загрузку). Так что это может также не стоить того, в зависимости от оборудования и соотношения вычислений и доступа к памяти.
Если вы пишете функцию, которая обычно вызывается с выровненными указателями, имеет смысл просто использовать невыровненные инструкции загрузки / сохранения. Любой пролог для обнаружения смещения - это просто дополнительные издержки для уже выровненного случая, и на современном оборудовании (Nehalem и новее) не выровненные нагрузки по адресу, которые оказываются выровненными во время выполнения, имеют производительность, идентичную выровненным инструкциям по нагрузке. (Но вам нужен AVX для невыровненных загрузок, чтобы сложить другие инструкции как операнды памяти. Например, vpxor xmm0, xmm1, [rsi]
)
Добавляя код для обработки неправильно выровненных входов, вы замедляете общий выровненный регистр, чтобы ускорить необычный не выровненный регистр. Быстрая аппаратная поддержка для выровненных загрузок / хранилищ позволяет программному обеспечению просто оставить это аппаратному обеспечению в тех немногих случаях, когда это происходит.
(Если неправильно выровненные входы являются общими, то для выравнивания входного указателя стоит использовать пролог, особенно если вы используете AVX. Последовательные 32-битные загрузки AVX будут разбивать строку кэша на каждую другую нагрузку.)
См. Руководство по сборке Agner Fog для получения дополнительной информации и другие ссылки в вики-теге x86.