2016-09-18 29 views
2

Мне нужно скопировать все нечетные пронумерованные байты из одного места памяти в другое. то есть скопировать первый, третий, пятый и т. д. В частности, я копирую из текстовой области 0xB8000, которая содержит 2000 символов/атрибутных слов. Я хочу пропустить байты атрибутов и просто закончить с символами. Следующий код работает отлично:инструкция SSE mov, которая может пропустить каждый второй байт?

 mov eax, ecx      ; eax = number of bytes (1 to 2000) 
     mov rsi, rdi      ; rsi = source 
     mov rdi, CMD_BLOCK     ; rdi = destination 
@@: movsb        ; copy 1 byte 
     inc rsi       ; skip the next source byte 
     dec eax 
     jnz @b  

число или символы, которые будут скопированы в любом месте от 1 до 2000. Я недавно начал играть с SSE2, SSE3 SSE4.2, но не может найти инструкцию (ы) что может уменьшить цикл. В идеале я хотел бы сократить петли с 2000 до 250, что было бы возможно, если бы была инструкция, которая могла пропустить каждый второй байт после загрузки 128 бит за раз.

ответ

2

Я хотел бы сделать что-то вроде этого, обработка 32 входных байт до 16 выходных байтов на итерации цикла:

const __m128i vmask = _mm_set1_epi16(0x00ff); 

for (i = 0; i < n; i += 16) 
{ 
    __m128i v0 = _mm_loadu_si128(&a[2 * i]);  // load 2 x 16 input bytes (MOVDQU) 
    __m128i v1 = _mm_loadu_si128(&a[2 * i + 16]); 
    v0 = _mm_and_si128(v0, vmask);    // mask unwanted bytes  (PAND) 
    v1 = _mm_and_si128(v1, vmask); 
    __m128 v = _mm_packus_epi16(v0, v1);   // pack low bytes   (PACKUSWB) 
    _mm_storeu_si128(v, &b[i];     // store 16 output bytes (MOVDQU) 
} 

Это C с встроенными функциями, конечно - если вы действительно хотите сделать это на ассемблере, то вам может просто преобразовать каждое внутреннее значение в соответствующую инструкцию.

+1

yup, именно то, о чем я думал. Выглядит лучше, чем любая комбинация PSHUFB, так как это только один случайный случай для каждого вектора результата, а перетасовки имеют более низкую пропускную способность, чем логические побитовые операции. –

+0

Я думаю, что этого достаточно, чтобы сделать шаг упаковки. – fuz

+1

Это именно то, что я надеялся найти. Очень признателен. – poby

2

Я вообще не использовал инструкции SIMD. Я сомневаюсь, что вы можете значительно повысить производительность 64-разрядных нагрузок, так как видеопамять отключена, и маловероятно, что шина поддерживает более широкие транзакции.

Я хотел бы использовать что-то вроде этого:

 lea rdi, [rdi + rcx * 2 - 8] 
loop: 
    mov rax, [rdi] 
    mov [CMD_BLOCK + rcx - 4], al 
    shr rax, 16 
    mov [CMD_BLOCK + rcx - 4 + 1], al 
    shr rax, 16 
    mov [CMD_BLOCK + rcx - 4 + 2], al 
    shr rax, 16 
    mov [CMD_BLOCK + rcx - 4 + 3], al 
    sub rdi, 8 
    sub rcx, 4 
    jnz loop 

Это выглядит неэффективно, но так как есть огромный срыв на нагрузке (mov rax,[rdi]) все остальное может происходить параллельно с этим.

Или в C:

void copy_text(void *dest, void *src, int len) { 
    unsigned long long *sp = src; 
    unsigned char *dp = dest; 
    int i; 

    for(i = 0; i < len; i += 4) { 
     unsigned long long a = *sp++; 
     *dp++ = (unsigned char) a; 
     a >>= 16; 
     *dp++ = (unsigned char) a; 
     a >>= 16; 
     *dp++ = (unsigned char) a; 
     a >>= 16; 
     *dp++ = (unsigned char) a; 
    } 
}  

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

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

+1

В (строго упорядоченной) UC-памяти вы не могли получить полную строку кэша с загрузками NT, как вы могли (из слабо упорядоченного) USWC, но вы все равно можете получить 16B в одной загрузке, не так ли? У Intel есть статья об использовании загрузок MOVNTDQA из видео mem: https://software.intel.com/en-us/articles/copying-accelerated-video-decode-frame-buffers. (Они используют хранилища NT в WB-памяти, с дополнительным трюком использования буфера отказов, который остается кэшированным, чтобы отделить загрузки NT от хранилищ NT, уменьшая заполнение частичной линии). –

+0

@PeterCordes Хм ... Я не знал инструкции MOVNTDQA. По-видимому, процессор позволяет игнорировать USWC-атрибут памяти и одновременно выполнять всю нагрузку на кеш-строку. Для видеопамяти, которая фактически находится в системной ОЗУ, которая должна быть победой (одна взрывная транзакция для DRAM), но я не знаю, будет ли это большим улучшением при чтении по шине PCI-Express. Я не уверен, что, как правило, поддерживаются более 64-разрядные чтения, инициированные процессором. –

+1

MOVNTDQA делает * не * переопределяет семантику упорядочения памяти, BTW. [См. Мой ответ здесь] (http://stackoverflow.com/questions/32103968/non-temporal-loads-and-the-hardware-prefetcher-do-they-work-together). В сильно-упорядоченной (WB) памяти это все еще сильно упорядоченная нагрузка. ЦП мог бы что-то сделать с подсказкой NT (например, избежать загрязнения кэша), поэтому он может быть полезен. Я только догадывался, не пытался проверить, как он реализован на современном Intel с большими * включенными * тегами кеша L3. –

2

Вы действительно используете 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, хотя это возможно).

+0

Это для обработчика клавиатуры в хобби os Я пишу, так что это не критически важно, но я люблю учиться и люблю писать наиболее эффективный код, который я могу особенно использовать с новыми инструкциями, с которыми я менее знаком. Как медленно читается и записывается в видеопамять? В сотни раз медленнее, чем в баране или в 2-3 раза медленнее? – poby

+1

@poby: Круто. Мне тоже не нравится неэффективный код. Но так как производительность цикла не имеет большого значения, лучше всего для * полной * производительности в этом случае, вероятно, сохранить небольшой размер кода, чтобы сократить выселение кэша команд. Так что, возможно, просто всегда используйте неуравновешенные нагрузки/магазины, особенно если вам не нужно избегать прочтения конца. Или даже скаляр, как предложил Росс. (Вероятно, объединение байтов в регистр для более широкого хранилища.) –

+1

@poby: re: видеопамять. IDK, но если он находится на видеокарте; в сотни или тысячи раз выше латентности, потому что он не может просто попасть в кеш L1. Я думаю, что пропускная способность может быть в порядке * если * вы широко читаете, особенно если вы используете MOVNTDQA для получения полной передачи строк в кеше. Если он находится в основной памяти (т. Е. Интегрированная графика, использующая память, физически подключенную к ЦП), то она, вероятно, по-прежнему остается нечитаемой. В сотни раз хуже латентности, чем нормальная область памяти WriteBack, но такая же пропускная способность, как и обычная память, если вы читаете с загрузкой SSE4.1 NT. –