Блит быстрее условного + приращение указателя? - PullRequest
0 голосов
/ 04 октября 2018

Это моя простая функция блинтинга:

static void blit8(unsigned char* dest, unsigned char* src)
{
    byte i;
    for (i = 0; i < 8; ++i) {
        if (*src != 0) {
            *dest = *src;
        }
        ++dest;
        ++src;
    }
}

Я уже на -O3, а blit8 находится на линии.restrict (gcc) здесь никак не влияет.Также не происходит приращения указателей каким-либо другим способом, или использование другого числа в качестве прозрачности, или другого типа для i ... Я даже пытался передать 1-байтовую битовую маску и проверить ее вместо разыменования src.Увеличение предела от i до, скажем, 16, кажется, обеспечивает очень незначительное ускорение (~ 4-6%), но я работаю с 8-байтовыми, а не 16-байтовыми блоками.

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

cg_annotate говорит об этом blit8 (без вставки):

Ir    I1mr   ILmr            Dr      D1mr   DLmr          Dw       D1mw    DLmw  file:function
3,747,585,536      62      1 1,252,173,824 2,097,653      0 674,067,968          0       0  ppu.c:blit8.constprop.0

Обычный cachegrind выход (с встраиванием):

I   refs:      6,446,979,546
I1  misses:          184,752
LLi misses:           22,549
I1  miss rate:          0.00%
LLi miss rate:          0.00%

D   refs:      2,150,502,425  (1,497,875,135 rd   + 652,627,290 wr)
D1  misses:       17,121,968  (    2,761,307 rd   +  14,360,661 wr)
LLd misses:          253,685  (       70,802 rd   +     182,883 wr)
D1  miss rate:           0.8% (          0.2%     +         2.2%  )
LLd miss rate:           0.0% (          0.0%     +         0.0%  )

LL refs:          17,306,720  (    2,946,059 rd   +  14,360,661 wr)
LL misses:           276,234  (       93,351 rd   +     182,883 wr)
LL miss rate:            0.0% (          0.0%     +         0.0%  )

0.8% D1 промах?Звучит довольно низко для меня.

Самым интересным для меня является то, что снятие проверки 0 (становящейся функционально идентичным memcpy) обеспечивает ускорение <1%, даже если: </p>

memcpy быстрее на ~ 25%.Я хочу максимально приблизиться к скорости необработанных memcpy, сохранив цвет 0 как прозрачный.

Проблема в том, насколько я знаю, ни одна векторная инструкция не поддерживает условные выражения, но янадо сохранить dest где src равно 0.Есть ли что-нибудь [быстрое], которое может действовать как OR, но на уровне байтов?

Я читал до того, как появилось расширение или что-то, чтобы сказать ЦПУ не кэшировать некоторые данные, но я не могу найтиэто снова.Моя идея состоит в том, чтобы не читать напрямую из src, а только записывать из него в dest и следить за тем, чтобы он не кэшировался.Затем просто прочитайте битовую маску, чтобы проверить прозрачность. Я просто не знаю, как на самом деле это сделать. Возможно ли это, не говоря уже о посте?Я тоже этого не знаю, поэтому и задаю этот вопрос.

Я бы предпочел советы о том, как сделать быстрее только с C, может быть, с некоторыми расширениями gcc, но если сборка x86 - единственный способ, быть по сему.Помочь мне понять мое фактическое узкое место (так как я смущен своими результатами) тоже помогло бы.

Ответы [ 2 ]

0 голосов
/ 07 октября 2018

Если ваш компилятор / архитектура поддерживает векторные расширения (например, clang и gcc), вы можете использовать что-то вроде:

//This may compile to awful code on x86_64 b/c mmx is slow (its fine on arm64)
void blit8(void* dest, void* src){
typedef __UINT8_TYPE__ u8x8  __attribute__ ((__vector_size__ (8), __may_alias__));
    u8x8 *dp = dest, d = *dp, *sp = src, s = *sp, cmp;
    cmp = s == (u8x8){0};
    d &= cmp;
    *dp = s|d;
}

//This may compile to better code on x86_64 - worse on arm64
void blit8v(void* dest, void* src){
typedef __UINT8_TYPE__ u8x16  __attribute__ ((__vector_size__ (16), __may_alias__));
typedef __UINT64_TYPE__ u64, u64x2  __attribute__ ((__vector_size__ (16), __may_alias__));
    u8x16 *dp = dest, d = *dp, *sp = src, s = *sp, cmp;
    cmp = s == (u8x16){0};
    d &= cmp;
    d |= s;
    *(u64*)dest = ((u64x2)d)[0];
}

//This one is fine on both arm and x86, but 16 bytes vs. 8
void blit16(void* dest, void* src){
typedef __UINT8_TYPE__ u8x16  __attribute__ ((__vector_size__ (16), __may_alias__));
    u8x16 *dp = dest, *sp = src, d = *dp, s = *sp, cmp;
    cmp = s == (u8x16){0};
    *dp = s|(d & cmp);
}

Компилирует на руку для:

blit8:
        ldr     d1, [x1]
        ldr     d2, [x0]
        cmeq    v0.8b, v1.8b, #0
        and     v0.8b, v0.8b, v2.8b
        orr     v0.8b, v0.8b, v1.8b
        str     d0, [x0]
        ret
blit16:
        ldr     q1, [x1]
        ldr     q2, [x0]
        cmeq    v0.16b, v1.16b, #0
        and     v0.16b, v0.16b, v2.16b
        orr     v0.16b, v0.16b, v1.16b
        str     q0, [x0]
        ret

на x86_64:

blit8v:                                 # @blit8v
        movdqa  xmm0, xmmword ptr [rsi]
        pxor    xmm1, xmm1
        pcmpeqb xmm1, xmm0
        pand    xmm1, xmmword ptr [rdi]
        por     xmm1, xmm0
        movq    qword ptr [rdi], xmm1
        ret
blit16:                                 # @blit16
        movdqa  xmm0, xmmword ptr [rsi]
        pxor    xmm1, xmm1
        pcmpeqb xmm1, xmm0
        pand    xmm1, xmmword ptr [rdi]
        por     xmm1, xmm0
        movdqa  xmmword ptr [rdi], xmm1
        ret
0 голосов
/ 05 октября 2018

Вы не упомянули, используете ли вы GCC или нет, но давайте предположим, что да.GCC требователен, если речь идет об условиях внутри циклов - вот почему ваш пример не может векторизовать.

Итак, этот код:

void blit8(unsigned char* dest, unsigned char* src)
{
    char i;
    for (i = 0; i < 8; ++i) {
        if (*src != 0) {
            *dest = *src;
        }
        ++dest;
        ++src;
    }
}

заканчивается как:

blit8:
        movzx   eax, BYTE PTR [rsi]
        test    al, al
        je      .L5
        mov     BYTE PTR [rdi], al
.L5:
        movzx   eax, BYTE PTR [rsi+1]
        test    al, al
        je      .L6
        mov     BYTE PTR [rdi+1], al
.L6:
        movzx   eax, BYTE PTR [rsi+2]
        test    al, al
        je      .L7
        mov     BYTE PTR [rdi+2], al
.L7:
        movzx   eax, BYTE PTR [rsi+3]
        test    al, al
        je      .L8
        mov     BYTE PTR [rdi+3], al
.L8:
        movzx   eax, BYTE PTR [rsi+4]
        test    al, al
        je      .L9
        mov     BYTE PTR [rdi+4], al
.L9:
        movzx   eax, BYTE PTR [rsi+5]
        test    al, al
        je      .L10
        mov     BYTE PTR [rdi+5], al
.L10:
        movzx   eax, BYTE PTR [rsi+6]
        test    al, al
        je      .L11
        mov     BYTE PTR [rdi+6], al
.L11:
        movzx   eax, BYTE PTR [rsi+7]
        test    al, al
        je      .L37
        mov     BYTE PTR [rdi+7], al
.L37:
        ret

Он был развернут компилятором, но все равно работает с одиночными байтами.

Но есть один прием, который довольно часто работает в таких случаях - вместо того, чтобы (cond) использовать троичный оператор.Это исправит одну проблему.Но есть и другой - GCC отказывается векторизовать короткий маленький блок - 8 байтов в этом примере.Итак, давайте воспользуемся другим приемом - выполняем вычисления для большего блока, но игнорируем его часть.

Вот мой пример:

void blit8(unsigned char* dest, unsigned char* src)
{
    int i;
    unsigned char temp_dest[16];
    unsigned char temp_src[16];

    for (i = 0; i < 8; ++i) temp_dest[i] = dest[i];
    for (i = 0; i < 8; ++i) temp_src[i] = src[i];

    for (i = 0; i < 16; ++i) 
    {
        temp_dest[i] = (temp_src[i] != 0) ? temp_src[i] : temp_dest[i];
    }

    for (i = 0; i < 8; ++i) dest[i] = temp_dest[i];
}

и соответствующая сборка:

blit8:
        mov     rax, QWORD PTR [rdi]
        vpxor   xmm0, xmm0, xmm0
        mov     QWORD PTR [rsp-40], rax
        mov     rax, QWORD PTR [rsi]
        mov     QWORD PTR [rsp-24], rax
        vmovdqa xmm1, XMMWORD PTR [rsp-24]
        vpcmpeqb        xmm0, xmm0, XMMWORD PTR [rsp-24]
        vpblendvb       xmm0, xmm1, XMMWORD PTR [rsp-40], xmm0
        vmovq   QWORD PTR [rdi], xmm0
        ret

ПРИМЕЧАНИЕ: я не тестировал его - это просто доказательство того, что SIMD-код может быть сгенерирован с использованием правильных правил и приемов кодирования;)

...