Как переместить 3 байта (24 бита) из памяти в регистр?

Я могу переместить элементы данных, хранящиеся в памяти, в регистр общего назначения по своему выбору, используя MOV инструкция.

MOV r8, [m8]
MOV r16, [m16]
MOV r32, [m32]
MOV r64, [m64]

Не стреляйте в меня, но как добиться следующего: MOV r24, [m24]? (Я ценю последнее не законно).

В моем примере я хочу переместить символы "Pip", то есть 0x706950h, чтобы зарегистрировать rax,

section .data           ; Section containing initialized data

14      DogsName: db "PippaChips"
15      DogsNameLen: equ $-DogsName

Сначала я подумал, что я мог бы перемещать байты отдельно, то есть сначала байт, затем слово или некоторую их комбинацию. Тем не менее, я не могу сослаться на "верхние половины" eax, rax, так что это падает с первого препятствия, так как в итоге я перезаписываю все данные, которые были перемещены первыми.

Мое решение:

26    mov al, byte [DogsName + 2] ; move the character “p” to register al
27    shl rax, 16                 ; shift bits left by 16, clearing ax to receive characters “pi”
28    mov ax, word [DogsName]     ; move the characters “Pi” to register ax

Я мог бы просто объявить "Pip" как инициализированный элемент данных, но в качестве примера я просто хочу понять, как ссылаться на 24 бит в сборке или 40, 48… в этом отношении.

Есть ли инструкция, больше похожая на MOV r24, [m24]? Есть ли способ выбора диапазона адресов памяти, в отличие от предоставления смещения и указания оператора размера. Как переместить 3 байта из памяти для регистрации в ASM x86_64?

NASM версия 2.11.08 Архитектура x86

3 ответа

Решение

Обычно вы выполняете 4-байтовую загрузку и маскируете большой мусор, который идет с желаемыми байтами, или просто игнорируете его, если вы что-то делаете с данными, которые не заботятся о старших битах. Какие целочисленные операции дополнения 2 можно использовать без обнуления старших битов на входах, если требуется только младшая часть результата?


В отличие от магазинов 1, загрузка данных, которые вы "не должны", никогда не является проблемой для правильности, если вы не перейдете на непроверенную страницу. (Например, если db "pip" пришел в конце страницы, а следующая страница была не отображена.) Но в этом случае вы знаете, что это часть более длинной строки, поэтому единственным возможным недостатком является производительность, если широкая загрузка распространяется на следующую строку кэша (так что нагрузка пересекает границу строки кэша). Безопасно ли читать за пределами буфера на одной и той же странице на x86 и x64?

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

mov   eax, [DogsName-1]     ; if previous byte is in the same page/cache line
shr   eax, 8

mov   eax, [DogsName]       ; if following byte is in the same page/cache line
and   eax, 0x00FFFFFF

Я предполагаю, что вы хотите обнулить результат в eax/rax, как 32-битный размер операнда, вместо слияния с существующими старшими байтами EAX/RAX, такими как 8 или 16-битный регистр размера операнда пишет. Если вы хотите объединить, замаскируйте старое значение и OR, Или если вы загрузили из [DogsName-1] поэтому нужные вам байты находятся в верхних 3 позициях EAX, и вы хотите слиться с ECX: shr ecx, 24 / shld ecx, eax, 24 чтобы сдвинуть старый верхний байт вниз до нижнего, а затем сдвинуть его назад, сдвигая 3 новых байта. (Там нет формы памяти-источника shld, к несчастью. Полусвязанный: эффективная загрузка из двух отдельных слов в слово.) shld работает на процессорах Intel (особенно Sandybridge и более поздних: 1 моп), но не на AMD ( http://agner.org/optimize/).


Сочетание 2 отдельных нагрузок

Есть много способов сделать это, но, к сожалению, нет единого самого быстрого способа для всех процессоров. Частичные записи в регистры ведут себя по-разному на разных процессорах. Ваш путь (загрузка байтов / сдвиг / загрузка слов в ax) довольно хорошо работает на процессорах, отличных от Core2/Nehalem (что приведет к вставке объединяющего оператора при чтении eax после его сборки). Но начнем с movzx eax, byte [DogsName + 2] сломать зависимость от старого значения rax,

Классический "безопасный везде" код, который вы ожидаете от компилятора, будет выглядеть так:

DEFAULT REL      ; compilers use RIP-relative addressing for static data; you should too.
movzx   eax, byte [DogsName + 2]   ; avoid false dependency on old EAX
movzx   ecx, word [DogsName]
shl     eax, 16
or      eax, ecx

Это требует дополнительной инструкции, но избегает записи каких-либо частичных регистров. Однако на процессорах, отличных от Core2 или Nehalem, лучшим вариантом для 2 нагрузок является запись ax, (Intel P6 до Core2 не может запустить код x86-64, и процессоры без частичного переименования регистров сливаются в rax при написании ax). Sandybridge по-прежнему переименовывает AX, но объединение стоит всего 1 моп без останова, то есть то же самое, что и операционная система, но на Core2 / Nehalem внешний интерфейс останавливается примерно на 3 цикла при вставке слияния.

Айвибридж и позже только переименовать AH не AX или же AL Таким образом, на этих процессорах загрузка в AX представляет собой микросинхронную загрузку + объединение. Agner Fog не перечисляет дополнительный штраф за mov r16, m в Silvermont или Ryzen (или любых других вкладках в электронной таблице, на которую я смотрел), поэтому предположительно выполняются и другие процессоры без переименования mov ax, [mem] в качестве нагрузки + слияния.

movzx   eax, byte [DogsName + 2]
shl     eax, 16
mov      ax, word [DogsName]

; using eax: 
  ; Sandybridge: extra 1 uop inserted to merge
  ; core2 / nehalem: ~3 cycle stall (unless you don't use it until after the load retires)
  ; everything else: no penalty

На самом деле, тестирование выравнивания во время выполнения может быть сделано эффективно. Учитывая указатель в регистре, предыдущий байт находится в той же строке кэша, если только последние 5 или 6 бит адреса не равны нулю. (т.е. адрес выравнивается по началу строки кэша). Предположим, что строки кэша составляют 64 байта; все текущие процессоры используют это, и я не думаю, что существуют какие-либо процессоры x86-64 с 32-байтовыми строками. (И мы все еще определенно избегаем пересечения страниц).

    ; pointer to m24 in RSI
    ; result: EAX = zero_extend(m24)

    test   sil, 111111b     ; test all 6 low bits.  There's no TEST r32, imm8, so  REX r8, imm8 is shorter and never slower.
    jz   .aligned_by_64

    mov    eax, [rsi-1]
    shr    eax, 8
.loaded:

    ...
    ret    ; end of whatever large function this is part of

 ; unlikely block placed out-of-line to keep the common case fast
.aligned_by_64:
    mov    eax, [rsi]
    and    eax, 0x00FFFFFF
    jmp   .loaded

Таким образом, в обычном случае дополнительная стоимость - это всего лишь один неиспользованный тестовый тест.

В зависимости от ЦП, входных данных и окружающего кода тестирование младших 12 битов (чтобы избежать пересечения границ 4 КБ) могло бы компенсировать лучшее предсказание ветвления для некоторых разбиений строк кэша на страницах, но все равно никогда не делило разделение строк кэша. (В таком случае test esi, (1<<12)-1, В отличие от тестирования sil с imm8, тестирование si с imm16 не стоит LCP на процессорах Intel, чтобы сэкономить 1 байт кода. И, конечно, если вы можете иметь указатель в ra/b/c/dx, вам не нужен префикс REX, и есть даже компактная 2-байтовая кодировка для test al, imm8.)

Вы могли бы даже сделать это без ответвлений, но явно не стоит того, чтобы делать две отдельные загрузки!

    ; pointer to m24 in RSI
    ; result: EAX = zero_extend(m24)

    xor    ecx, ecx
    test   sil, 7         ; might as well keep it within a qword if  we're not branching
    setnz  cl             ; ecx = (not_start_of_line) ? : 1 : 0

    sub    rsi, rcx       ; normally rsi-1
    mov    eax, [rsi]

    shl    ecx, 3         ; cl = 8 : 0
    shr    eax, cl        ; eax >>= 8  : eax >>= 0

                          ; with BMI2:  shrx eax, [rsi], ecx  is more efficient

    and    eax, 0x00FFFFFF  ; mask off to handle the case where we didn't shift.

Настоящая архитектурная 24-битная загрузка или хранение

Архитектурно, x86 не имеет 24-битных загрузок или хранилищ с целочисленным регистром в качестве места назначения или источника. Как указывает Брэндон, магазины в масках MMX / SSE (например, MASKMOVDQU, не путать с pmovmskb eax, xmm0) может хранить 24 бита из регистров MMX или XMM, учитывая векторную маску с установленными только младшими 3 байтами. Но они почти никогда не полезны, потому что они медленные и всегда имеют подсказку NT (поэтому они пишут по кешу и принудительно выталкивают как movntdq). (Инструкция загрузки / сохранения маски dword / qword AVX не подразумевает NT, но не доступна с байтовой гранулярностью.)

AVX512BW (Skylake-сервер) добавляет vmovdqu8 которая дает вам байтовую маскировку для нагрузок и сохраняет подавление ошибок для байтов, которые замаскированы. (То есть, вы не будете работать с сегфоутом, если 16-байтовая загрузка включает байты на не отображенной странице, если биты маски не установлены для этого байта. Но это вызывает большое замедление). Таким образом, в микроархитектуре это по-прежнему нагрузка в 16 байт, но влияние на архитектурное состояние (т. Е. Все, кроме производительности) в точности влияет на истинную 3-байтовую загрузку / хранение (с правильной маской).

Вы можете использовать его в регистрах XMM, YMM или ZMM.

;; probably slower than the integer way, especially if you don't actually want the result in a vector
mov       eax, 7                  ; low 3 bits set
kmovw     k1, eax                 ; hoist the mask setup out of a loop


; load:  leave out the {z} to merge into the old xmm0 (or ymm0 / zmm0)
vmovdqu8  xmm0{k1}{z}, [rsi]    ; {z}ero-masked 16-byte load into xmm0 (with fault-suppression)
vmovd     eax, xmm0

; store
vmovd     xmm0, eax
vmovdqu8  [rsi]{k1}, xmm0       ; merge-masked 16-byte store (with fault-suppression)

Это собирается с NASM 2.13.01. IDK, если ваш NASM достаточно новый для поддержки AVX512. Вы можете играть с AVX512 без аппаратного обеспечения, используя Эмулятор разработки программного обеспечения Intel (SDE)

Это выглядит круто, потому что это всего 2 мопа, чтобы получить результат в eax (после настройки маски). (Однако электронная таблица данных http://instlatx64.atw.hu/ из IACA для Skylake-X не включает vmovdqu8 с маской, только немаскированные формы. Они указывают на то, что это все еще единичная загрузка, или магазин с микроплавлением, как обычный vmovdqu/a)

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

Кроме того, для версии магазина будьте осторожны, поскольку маскированные магазины не так эффективно перенаправляют загрузку. (См. Руководство по оптимизации Intel для более подробной информации).


Примечания:

  1. Широкие хранилища представляют собой проблему, потому что даже если вы замените старое значение, вы будете делать неатомарное чтение-изменение-запись, что может привести к поломке, если, например, тот байт, который вы вернули, был блокировкой. Не храните вне предметов, если вы не знаете, что будет дальше и что это безопасно, например, заполнение, которое вы положили туда, чтобы разрешить это. Вы могли бы lock cmpxchg измененное 4-байтовое значение на месте, чтобы убедиться, что вы не наступаете на обновление дополнительного байта другим потоком, но очевидно, что выполнение двух отдельных хранилищ намного лучше для производительности, чем атомарный cmpxchg повторите цикл.

Единственный способ записать 24 бита - использовать MMX (MASKMOVQ) или SSE (MASMODQU) и маски для предотвращения изменения байтов, которые вы не хотите изменять. Однако для одной записи MMX и SSE чрезмерно сложны (и, вероятно, медленнее).

Обратите внимание, что обычно чтение дешевле, чем запись (особенно, когда задействованы несколько процессоров). Имея это в виду, альтернативой будет:

    shl eax,8
    mov al,[DogsName+3]
    ror eax,8
    mov [DogsName],eax

Это перезаписывает байт after с его старым значением (и может потенциально вызвать проблемы, если байт after недоступен или если byte after принадлежит чему-либо, что должно быть обновлено атомарно).

С BMI2 вы можете использовать BZHI

BZHI r32a, r/m32, r32b   Zero bits in r/m32 starting with the position in r32b, write result to r32a
BZHI r64a, r/m64, r64b   Zero bits in r/m64 starting with the position in r64b, write result to r64a

Таким образом, чтобы загрузить младшие 24 бита из [mem] ты можешь использовать

MOV  eax, 24
BZHI eax, [mem], eax

При этом вы также можете загрузить переменное количество бит из памяти

Другие вопросы по тегам