Вы действительно используете SIMD для видеопамяти VGA в режиме x86-64 Режим? Это забавно, но на самом деле правдоподобно в реальной жизни и работает в качестве прецедента для некоторых манипуляций с данными SIMD.
Однако, если вы действительно читаете из видеопамяти, вы можете делать неквалифицированные нагрузки, что плохо, и подразумевает, что вы должны перепроектировать вашу систему, чтобы вам не пришлось это делать. (См. Ответ Росса для предложений)
В видеопамяти USWC вы можете получить большое ускорение от MOVNTDQA. См. Intel's article, а также пару моих ответов о загрузках NT: here и особенно this one, где я объясняю, что в инструкциях x86 ISA говорят о загрузке NT, не переопределяя семантику упорядочения памяти, поэтому они не слабо упорядочены, если вы не используете их в слабо- упорядоченных областей памяти.
Как вы подозревали, вы не найдете инструкции по копированию в наборах инструкций SIMD; вы должны сами обрабатывать данные в регистрах между загрузками и магазинами. Нет ни одной инструкции SSE/AVX, которая сделает это за вас. (ARM NEON unzip instruction действительно решает всю проблему).
Вы должны использовать SSE2 PACKUSWB, чтобы упаковать два вектора (подпись) int16_t к одному вектору uint8_t. После обнуления верхнего байта каждого словарного элемента, насыщающегося до 0..255, не будет изменять ваши данные вообще.
Вот настоящая (непроверенная) петля, которая выравнивает указатель источника, чтобы минимизировать штрафы за пересечение границ кеш-линии, и использует некоторые трюки в режиме адресации для сохранения инструкций в цикле.
Несвязанные грузы имеют очень мало штрафных санкций в Nehalem, а затем, в основном, за дополнительную задержку, когда они пересекают границу линии кэша. Так что это в основном полезно, если вы хотите использовать загрузки NT из видеопамяти. Или это может быть полезно, если вы в противном случае читали бы за пределами конца src в конце больших копий.
Мы делаем в два раза больше загрузок, чем магазинов, поэтому, если загрузка с загрузкой/хранением связана с выгруженными нагрузками (вместо ориентированных магазинов), может быть оптимальной. Тем не менее, слишком много работы ALU для насыщения загрузки/хранения кеша, поэтому , сохраняя его простым с невыложенными нагрузками (например, петля Paul R), должно работать очень хорошо на большинстве процессоров и прецедентов.
mov edx, CMD_BUFFER ; or RIP-relative LEA, or hopefully this isn't even static in the first place and this instruction is something else
;; rdi = source ; yes this is "backwards", but if you already have the src pointer in rdi, don't waste instructions
;; rcx = count
;; rdx = dest
pcmpeqw xmm7, xmm7 ; all ones (0xFF repeating)
psrlw xmm7, 8 ; 0x00FF repeating: mask for zeroing the high bytes
;cmp ecx, 16
;jb fallback_loop ; just make CMD_BUFFER big enough that it's ok to copy 16 bytes when you only wanted 1. Assuming the src is also padded at the end so you can read without faulting.
;; First potentially-unaligned 32B of source data
;; After this, we only read 32B chunks of 32B-aligned source that contain at least one valid byte, and thus can't segfault at the end.
movdqu xmm0, [rdi] ; only diff from loop body: addressing mode and unaligned loads
movdqu xmm1, [rdi + 16]
pand xmm0, xmm7
pand xmm1, xmm7
packuswb xmm0, xmm1
movdqu [rdx], xmm0
;; advance pointers just to the next src alignment boundary. src may have different alignment than dst, so we can't just AND both of them
;; We can only use aligned loads for the src if it was at least word-aligned on entry, but that should be safe to assume.
;; There's probably a way to do this in fewer instructions.
mov eax, edi
add rdi, 32 ; advance 32B
and rdi, -32 ; and round back to an alignment boundary
sub eax, edi ; how far rdi actually advanced
shr eax, 1
add rdx, rax ; advance dst by half that.
;; if rdi was aligned on entry, the it advances by 32 and rdx advances by 16. If it's guaranteed to always be aligned by 32, then simplify the code by removing this peeled unaligned iteration!
;; if not, the first aligned loop iteration will overlap some of the unaligned loads/store, but that's fine.
;; TODO: fold the above calculations into this other loop setup
lea rax, [rdx + rdx]
sub rdi, rax ; source = [rdi + 2*rdx], so we can just increment our dst pointer.
lea rax, [rdx + rcx] ; rax = end pointer. Assumes ecx was already zero-extended to 64-bit
; jmp .loop_entry ; another way to check if we're already done
; Without it, we don't check for loop exit until we've already copied 64B of input to 32B of output.
; If small inputs are common, checking after the first unaligned vectors does make sense, unless leaving it out makes the branch more predictable. (All sizes up to 32B have identical branch-not-taken behaviour).
ALIGN 16
.pack_loop:
; Use SSE4.1 movntdqa if reading from video RAM or other UCSW memory region
movdqa xmm0, [rdi + 2*rdx] ; indexed addressing mode is ok: doesn't need to micro-fuse because loads are already a single uop
movdqa xmm1, [rdi + 2*rdx + 16] ; these could optionally be movntdqa loads, since we got any unaligned source data out of the way.
pand xmm0, xmm7
pand xmm1, xmm7
packuswb xmm0, xmm1
movdqa [rdx], xmm0 ; non-indexed addressing mode: can micro-fuse
add rdx, 16
.loop_entry:
cmp rdx, rax
jb .pack_loop ; exactly 8 uops: should run at 1 iteration per 2 clocks
;; copies up to 15 bytes beyond the requested amount, depending on source alignment.
ret
С неразрушающим 3 кодирования операндов AVX, в нагрузки могут быть сложены в vpand xmm0, xmm7, [rdi + 2*rdx]
(тавра). Но indexed addressing modes can't micro-fuse on at least some SnB-family CPUs, поэтому вы, вероятно, захотите развернуть и add rdi, 32
, а также add rdx, 16
вместо того, чтобы использовать трюк обращения к источнику относительно адресата.
AVX принесет тело цикла до 4-х слитых доменов для 2xload + и/pack/store, плюс накладные расходы на цикл. С разворачиванием мы могли бы начать приближаться к теоретической максимальной пропускной способности Intel Haswell с 2 нагрузками + 1 магазин за такт (хотя он не может выдержать этого; адрес хранилища uops будет украсть p23-циклы вместо использования p7 иногда. Руководство по оптимизации Intel предоставляет реальную -Мировой устойчивое количество пропускной способности что-то вроде ~ 84В загружается и сохраняется за такт (с использованием 32-байтовых векторов), считая все хиты кэша L1, что меньше, чем пиковая пропускная способность 96В.)
Вы также могли бы использовать byte shuffle (SSSE3 PSHUFB), чтобы получить четные байты вектора, упакованного в младшие 64 бит. (Затем выполните одно 64-битное хранилище MOVQ для каждой 128-битной нагрузки или объедините две нижние половины с PUNPCKLQDQ). Но это отстой, потому что (на 128-битный вектор исходных данных), это 2 тасования + 2 магазина или 3 тасования + 1 магазин. Вы можете сделать слияние более дешевым, используя различные маски тасования, например. перетасуйте четные байты в нижнюю половину одного вектора и верхнюю половину другого вектора. Поскольку PSHUFB также может бесплатно обрезать любые байты, вы можете комбинировать их с POR (а не немного дороже PBLENDW или AVX2 VPBLENDD). Это 2 перетасовки + 1 булев + 1 магазин, все еще узкое место при перемещении.
Метод PACKUSWB представляет собой 2 булевских ops + 1 shuffle + 1 store (меньше узкого места, потому что PAND может работать на дополнительных портах выполнения, например, 3 за такт против 1 за часы для перетасовки).
AVX512BW (доступно на Skylake-avx512 but not on KNL) обеспечивает
VPMOVWB ymm1/m256 {k1}{z}, zmm2
(__m256i _mm512_cvtepi16_epi8 (__m512i a)
), который пакеты с усечением вместо насыщения. В отличие от команд пакета SSE, он принимает только 1 вход и дает более узкий результат (который может быть местом памяти). (vpmovswb
и vpmovuswb
аналогичны, и пакет с подписанной или беззнаковой насыщенностью. Имеются все одинаковые размеры, такие как pmovzx
, например vpmovqb xmm1/m64 {k1}{z}, zmm2
, поэтому вам не нужно несколько шагов. Размеры источников Q и D указаны в AVX512F).
Функциональность памяти-dest даже отображается с встроенным C/C++, что позволяет удобно закодировать маскированный магазин в C. (Это приятное изменение от pmovzx
where it's inconvenient to use intrinsics and get the compiler to emit a pmovzx
load).
AVX512VBMI (ожидается в Intel Cannonlake) может сделать два входа один выход 512b с одним VPERMT2B, учитывая перетасовка маску, которая берет даже байты из двух входных векторов и производит один результирующий вектор.
Если VPERM2TB работает медленнее, чем VPMOVWB, использование VPMOVWB для одного вектора за раз, вероятно, будет лучшим. Даже если они имеют одинаковую пропускную способность/латентность/uop-count, коэффициент усиления может быть настолько малым, что не стоит делать другую версию и обнаруживать AVX512VBMI вместо AVX512BW. (Вряд ли CPU может иметь AVX512VBMI без AVX512BW, хотя это возможно).
yup, именно то, о чем я думал. Выглядит лучше, чем любая комбинация PSHUFB, так как это только один случайный случай для каждого вектора результата, а перетасовки имеют более низкую пропускную способность, чем логические побитовые операции. –
Я думаю, что этого достаточно, чтобы сделать шаг упаковки. – fuz
Это именно то, что я надеялся найти. Очень признателен. – poby