SSE: невысокая загрузка и сохранение, которая пересекает границу страницы

Я где-то читал, что перед выполнением несвязанной загрузки или сохранения рядом с границей страницы (например, с использованием _mm_loadu_si128/_mm_storeu_si128 intrinsics) код должен сначала проверить, принадлежит ли весь вектор (в данном случае 16 байт) к той же странице, и переключитесь на не-векторные инструкции, если нет. Я понимаю, что это необходимо для предотвращения coredump, если следующая страница не принадлежит процессу.

Но что, если обе страницы принадлежат процессу (например, они являются частью одного буфера, и я знаю размер этого буфера)? Я написал небольшую тестовую программу, которая выполняла неравномерную загрузку и сохраняла границу пересеченной страницы, и она не сработала. Должен ли я всегда проверять границу страницы в этом случае, или этого достаточно, чтобы убедиться, что я не буду переполнять буфер?

Env: Linux, x86_64, gcc

Ответ 1

Разделители строк страницы неэффективны для производительности, но не влияют на правильность несвязанных доступов. Достаточно убедиться, что вы не читаете за пределами конца буфера, когда вы знаете длину раньше времени.


Для правильности вам часто приходится беспокоиться об этом при реализации чего-то вроде strlen, где ваш цикл останавливается, когда вы находите значение дозорного. Это значение может быть в любой позиции внутри вашего вектора, так что просто выполнение 16B невысоких нагрузок будет считываться за конец массива. Если завершающий 0 находится в последнем байте одной страницы, а следующая страница не читается, а указатель текущей позиции не выравнивается, то нагрузка, содержащая байт 0, также будет содержать байты с нечитаемой страницы, поэтому он будет виноват.

Одним из решений является сканирование до тех пор, пока ваш указатель не будет выровнен, а затем выровняйте выровненные векторы. Выровненная нагрузка всегда полностью исходит из одной страницы, а также из одной строки кеша. Поэтому, несмотря на то, что вы прочтете несколько байтов за конец строки, вы гарантированно не получите ошибку. Valgrind может быть недовольна этим, но стандартная библиотека strlen использует это использование.

Вместо скаляра до выровненного указателя вы можете выполнить неравнозначный вектор с начала строки (если это не пересекает линию страницы), а затем выполнить выравнивание нагрузок. Первая выровненная нагрузка будет перекрывать первую неравновесную нагрузку, но это абсолютно нормально для функции типа strlen, которая не заботится о том, будет ли она видеть одни и те же данные дважды.


Возможно, стоит избегать расщепления страниц по соображениям производительности. Даже если вы знаете, что ваш указатель src неверен, он часто быстрее разрешает разделение кэша линии аппаратного обеспечения. Но до Skylake, разбиение страниц имеет дополнительную задержку ~ 100 с. (До 5c в Skylake). Если у вас есть несколько указателей, которые могут быть выровнены по-разному относительно друг друга, вы не всегда можете просто использовать пролог, чтобы выровнять ваш src. (например, c[i] = a[i] + b[i] и c выровнены, но b нет.)

В этом случае, возможно, стоит использовать ветку для выравнивания нагрузок до и после разделения страницы и объединить их с palignr.

Неправильное предсказание ветки (~ 15c) дешевле, чем задержка с разбиением на страницы, но задерживает все (а не только нагрузку). Таким образом, это также не стоило бы того, в зависимости от аппаратного обеспечения и соотношения вычислений в доступе к памяти.


Если вы пишете функцию, которая обычно вызывается с помощью ориентированных указателей, имеет смысл просто использовать нестандартные команды загрузки/хранения. Любой пролог для обнаружения несоосности - это дополнительные накладные расходы для уже выровненного случая, а на современном оборудовании (Nehalem и новее) неодинаковые нагрузки на адрес, которые оказываются выровненными во время выполнения, имеют идентичную производительность для согласованных инструкций загрузки. (Но вам нужно, чтобы AVX для невыложенных нагрузок складывался в другие команды в качестве операндов памяти, например vpxor xmm0, xmm1, [rsi])

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

(Если неравнозначные входы являются общими, то стоит использовать пролог, чтобы выровнять указатель на вход, если вы используете AVX. Последовательные загрузки 32-битных AVX будут разделяться на кеш-линию при каждом другом загрузке.)

Смотрите Agner Fog Optimizing Assembly guide для получения дополнительной информации и других ссылок в tag wiki.