ошибка страницы maskmovdqu / _mm_maskmoveu_si128 - как избежать?

У меня есть функция, которая выводит структурированные данные. Данные представляют собой структуры Vec4/Vec3/Vec2/float, поэтому максимальный размер составляет 16 байт на структуру. Теперь может случиться так, что поток читается, начиная с структуры. Простое решение: загрузите структуру, создайте маску хранилища, уменьшите указатель данных назначения на количество байтов в нашей структуре, которую вызов хочет начать читать.

Представьте, что текущий тип элемента — Vec2, у нас 4 байта в этой структуре:

xmm0 = 00000000-00000000-dadadada-dadadada
xmm1 = 00000000-00000000-ffffffff-00000000
result_data_ptr = 13450000
-> RDI = 1344fffc
maskmovdqu xmm0, xmm1

=> результат является исключением ошибки страницы.

Есть ли способ обнаружить, что эта ошибка страницы произойдет? Память о предыдущей странице даже не тронется...


person St0fF    schedule 11.10.2019    source источник
comment
Я бы рекомендовал избегать maskmovdqu (это странно и медленно), но то, что это повлечет за собой, зависит от того, как вы его использовали.   -  person harold    schedule 11.10.2019
comment
Ну, я попытался описать, как это было использовано. Может недостаточно четко. Я получаю указатель данных от вызывающей стороны =>, то есть result_data_ptr. Объект потока вычисляет, сколько байт внутри текущего элемента. Создает маску магазина в xmm1, хранит сам элемент в xmm0 и RDI:=result_data_ptr-bytes_inside. Теперь, если result_data_ptr была границей страницы, а предыдущая страница не принадлежала моему пространству памяти приложения, я получаю ошибку этой страницы.   -  person St0fF    schedule 11.10.2019


Ответы (2)


maskmovdqu не выполняет подавление ошибок, в отличие от AVX vmaskmovps или маскированных хранилищ AVX512. Это решит вашу проблему, хотя, возможно, и не самым эффективным способом.

Как описано в справочном руководстве Intel по ISA, с нулевой маской (поэтому ничего не сохраняется для память) Исключения, связанные с адресацией памяти и ошибками страниц, все еще могут сигнализироваться (зависит от реализации).

Я предполагаю, что с ненулевой маской гарантируется, что она совершит ошибку страницы, если 16 байтов включают какие-либо страницы, недоступные для записи. Или, возможно, в некоторых реализациях маска подавляет ошибки, даже когда происходит некоторое сохранение (нули на неотображенной странице, но ненулевые в других местах)


В любом случае, на реальных процессорах это не быстрая инструкция.

maskmovdqu иногда мог быть хорош на одноядерном Pentium 4 (или не на IDK), и/или его предшественник MMX мог быть полезен на Pentium в порядке очереди. Сохранения с обходом маскированного кеша гораздо менее полезны на современных процессорах, где L3 является обычным резервом, а кеши большие. Возможно, более важно то, что между одним ядром и контроллером(ами) памяти существует больше механизмов, потому что все должно работать правильно, даже если другое ядро ​​делало перезагрузку этой памяти в какой-то момент, поэтому запись неполной строки возможна. может быть, даже менее эффективным.

Как правило, это ужасный выбор, если вы действительно храните всего 8 или 12 байтов. (В основном то же самое, что и хранилище NT, которое не записывает полную строку). Особенно, если вы используете несколько узких хранилищ для захвата фрагментов данных и помещения их в один непрерывный поток. Я бы не предполагал, что несколько перекрывающихся maskmovdqu хранилищ приведут к одному эффективному хранилищу всей строки кэша, как только вы в конечном итоге закончите одно, даже если маски означают, что ни один байт фактически не записывается дважды.

Кэш L1d отлично подходит для буферизации нескольких небольших операций записи в строку кэша до того, как они в конечном итоге будут выполнены; используйте эти обычные хранилища, если вы не можете сделать несколько хранилищ NT почти вплотную.

Чтобы сохранить верхние 8 байтов регистра XMM, используйте movhps.

Запись в кеш также позволяет делать перекрывающиеся хранилища, например movdqu. Таким образом, вы можете объединить несколько 12-байтовых объектов, перетасовав их каждый в конец регистра XMM (или загрузив их таким образом в первую очередь), а затем использовать movdqu для сохранения в [rdi], [rdi+12], [rdi+24] и т. д. перекрытие совершенно нормально; объединение в буфере хранилища может поглотить его еще до того, как он будет зафиксирован в кэше L1d, а если нет, то кэш L1d все еще будет довольно быстрым.


В начале записи большого массива, если вы не знаете выравнивание, вы можете сделать невыровненные movdqu из первых 16 байтов вашего вывода. Затем сделайте первое выровненное хранилище по 16 байтам, возможно, перекрывающееся с этим. Если ваш общий выходной размер всегда> = 16 байт, эта стратегия не требует большого количества ветвлений, чтобы вы могли выполнять выровненные хранилища для большей части этого. В конце вы можете сделать то же самое с окончательным потенциально невыровненным вектором, который может частично перекрывать последний выровненный вектор. (Или, если массив выровнен, то перекрытия нет, и он тоже выровнен. movdqu работает так же быстро, как movdqa, если адрес выровнен на современных процессорах.)

person Peter Cordes    schedule 11.10.2019
comment
Я читал эти замечания в руководстве, поэтому и спросил в первую очередь. Но, судя по вашему ответу, правильного пути нет. Я опубликую самостоятельный ответ, чтобы показать, как обойти в этой конкретной обстановке. - person St0fF; 11.10.2019
comment
@St0fF: есть несколько правильных способов, например. АВХ vmaskmovps. Или SSE4.1 extractps [rdi], xmm0, 2 будет хранить только 3-й элемент как хранилище двойных слов, а не маскированное 16-байтовое хранилище. Или с SSE1, shufps + movss. - person Peter Cordes; 11.10.2019
comment
Я имел в виду, что правильные способы обнаружения maskmovdqu приведут к ошибке страницы... - person St0fF; 11.10.2019
comment
@St0fF: О. Вы можете просто обнаружить пересечение страниц, проверив, p&0xfff >= (4096-15). то есть проверьте биты смещения страницы адреса. Вы, вероятно, все равно захотите избежать разделения страниц, даже если предыдущая страница отображается. Или вы не имели в виду определить, будет ли maskmovdqu вообще выполнять подавление ошибок на одном процессоре по сравнению с другим? То, как написано в инструкции, ни о чем не говорит. С ненулевой маской ничего не упоминается о подавлении ошибок, поэтому мы не должны ожидать, что они есть. Вы можете проверить руководство vol3; в ручном вводе ISA ref не всегда есть все. - person Peter Cordes; 11.10.2019
comment
@St0fF: но в отношении обнаружения во время выполнения, чтобы избежать разделения страниц. Скорее всего, было бы лучше всегда избегать maskmovdqu, даже если это не будет разделением страниц. Но разделение страниц в целом происходит очень медленно на процессорах Intel до Skylake, например, на 100 дополнительных циклов. (По крайней мере, у загрузок с разделением страниц есть такой штраф, я забыл, тестировал ли я магазины. Они не очень хороши даже на Skylake, все же дороже, чем другие хранилища с разделением строк кеша. По крайней мере, для обычных кэшируемых хранилищ. Я не проверял маскмов.) - person Peter Cordes; 11.10.2019
comment
Я помечаю ваш ответ как ответ, так как он просто содержит очень много полезной информации. - person St0fF; 13.10.2019

Что ж, поскольку кажется, что нет хорошего способа предсказать ошибку страницы, я пошел другим путем. Это прямое решение asm:

Во-первых, мы используем таблицу для сдвига результата в соответствии с bytes_inside. Затем узнаем, сколько байт нужно записать. Поскольку необходимо записать не более 15 байтов, это работает как 4-этапный процесс. Мы просто проверяем биты bytes_to_write — если установлен бит «8» (то есть бит 3), мы используем movq. для бита 2 требуется movd, для бита 1 — pextrw, а для бита 0 — pextrb. После каждого сохранения указатель данных соответственно увеличивается, и соответственно сдвигается регистр данных.

Регистры:

  • r10: result_data_ptr
  • r11: байты_внутри
  • xmm0.word[6]: размер элемента данных
  • xmm2: наш элемент данных
  • shuf_inside: таблица данных для побайтового вращения регистра xmm с использованием pshufb (psrldq позволяет только немедленный подсчет байтового сдвига)
.DATA
ALIGN 16
shuf_inside   byte 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0
              byte 2,3,4,5,6,7,8,9,10,11,12,13,14,15,0,1
              byte 3,4,5,6,7,8,9,10,11,12,13,14,15,0,1,2
              byte 4,5,6,7,8,9,10,11,12,13,14,15,0,1,2,3
              byte 5,6,7,8,9,10,11,12,13,14,15,0,1,2,3,4
              byte 6,7,8,9,10,11,12,13,14,15,0,1,2,3,4,5
              byte 7,8,9,10,11,12,13,14,15,0,1,2,3,4,5,6
              byte 8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7
              byte 9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,8
              byte 10,11,12,13,14,15,0,1,2,3,4,5,6,7,8,9
              byte 11,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10
              byte 12,13,14,15,0,1,2,3,4,5,6,7,8,9,10,11
              byte 13,14,15,0,1,2,3,4,5,6,7,8,9,10,11,12
              byte 14,15,0,1,2,3,4,5,6,7,8,9,10,11,12,13
              byte 15,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14
.CODE
[...]
        lea             rax,        [ shuf_inside ]
        shl             r11,        4
        pshufb          xmm2,       [ rax + r11 - 16 ]
        shr             r11,        4
        pextrw          rax,        xmm0,       6           ;reducedStrideWithPadding - i.e. size of item
        sub             rax,        r11                     ;bytes_to_write
        ;
        test            rax,        8
        jz              lessThan8
        movq            qword ptr [r10], xmm2
        psrldq          xmm2,       8
        add             r10,        8
        lessThan8:
        test            rax,        4
        jz              lessThan4
        movd            dword ptr [r10], xmm2
        psrldq          xmm2,       4
        add             r10,        4
        lessThan4:
        test            rax,        2
        jz              lessThan2
        pextrw          word ptr [r10], xmm2, 0
        psrldq          xmm2,       2
        add             r10,        2
        lessThan2:
        test            rax,        1
        jz              lessThan1
        pextrb          byte ptr [r10], xmm2, 0
        lessThan1:
person St0fF    schedule 11.10.2019
comment
Если вам важен размер кода, используйте test al, 8 вместо RAX. 2 байта против 6, потому что нет версии test с расширенным знаком 8-битного непосредственного для 32- или 64-битного размера операнда. - person Peter Cordes; 11.10.2019
comment
Но на самом деле это выглядит довольно неэффективно. Я бы рассмотрел возможность загрузки 16 байтов из места назначения, а затем смешал ваши новые байты и сохранил. Вводит неатомарное RMW, но, по-видимому, ни один другой поток не записывает то же место в то же время. Вы можете создать маску перехода с невыровненной нагрузкой, которая охватывает границу между 0xffff... и 0x0000.... См. Векторизация с невыровненными буферами: использование VMASKMOVPS: создание маски из счетчика смещения? в качестве примера 31-байтового скользящего окна. из 16x 16-байтовых векторов. - person Peter Cordes; 11.10.2019
comment
Вы уже используете SSE4.1 pextrb, поэтому можете использовать pblendvb. Выполнение байтового сдвига с переменным числом - это боль, но вы можете построить его из pshufb, как вы делаете сейчас, или, может быть, ветвиться со сдвигом › 8 или нет и построить его из psrlq / psllq + перемешивание и por. Вероятно, поиск в скользящем окне управляющего вектора pshufb для эмуляции числа переменных psrldq хорош, сдвигая нули (старший бит управляющего вектора). Вы можете использовать одну и ту же таблицу дважды: один раз, чтобы поместить данные в нужное место в вашем векторе, и второй раз, чтобы создать маску смешивания из вектора, состоящего из всех единиц (созданного с помощью pcmpeqd same,same) - person Peter Cordes; 11.10.2019
comment
Большое спасибо. Все эти намеки звучат очень интересно. До сих пор я ограничивал себя тем, что не использовал команды AVX или Vxxxx, так как по крайней мере одна из моих целевых систем все еще использует архитектуру Nehalem. Во всяком случае, мне также нравится идея двух замаскированных загрузок, смешивания и записи через movdqu. Звучит намного лучше, чем приведенное выше решение, и оно устраняет необходимость в maskmovdqu. - person St0fF; 13.10.2019
comment
Кроме того, у меня была еще одна мысль о моей функции: она всегда вызывается в начале гораздо более крупной операции, поэтому даже не имеет значения, будут ли следующие байты затерты. Следующие операции будут хранить там нужные данные, скорее всего, пока эта строка кеша даже не была зафиксирована, то есть вскоре после этого. - person St0fF; 13.10.2019
comment
Да, один из абзацев моего ответа касался этого. Кэш L1d отлично работает в качестве буфера объединения записей для поглощения перекрывающихся хранилищ векторов. (Кстати, я обычно использую термин фиксация для store-buffer -> кэш L1d, потому что именно в этот момент он становится глобально видимым. Кэш непротиворечив. Если вы не говорите о постоянной памяти, такой как энергонезависимые модули DIMM, в L1d = зафиксирован. Срок, который вам нужен, - это до того, как строка кэша будет вытеснена из L1d.) Или слияние может даже произойти в буфере хранилища до фиксации в L1d, для последовательного сохранения в одну и ту же строку. - person Peter Cordes; 13.10.2019