LINUX.ORG.RU

быстрый xor на amd x86_64


0

1

Здравстуйте, есть задача сделать быстрый xor для большого буфера. Имеется процессор Quad-Core AMD Opteron(tm) Processor 2346 HE,
flags      : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx mmxext fxsr_opt rdtscp lm 3dnowext 3dnow constant_tsc rep_good tsc_reliable nonstop_tsc pni cx16 popcnt hypervisor lahf_lm extapic abm sse4a misalignsse 3dnowprefetch

Делаю 3 варианта для теста, те что пришли в голову:
Вариант 1, самай простой через xorb:
for (i = 0; i < len; i++)
buf[i] ^= (char)key;

asm:
400780: 80 34 18 0c xorb $0xc,(%rax,%rbx,1)

Вариант 2, xor на 64 битных регистрах + хвост по байтам (xor инструкция):

__uint64_t *lbuf = (__uint64_t *)buf;
int llen = len % sizeof(__uint64_t);
for (i = 0; i < llen; i++)
lbuf[i] ^= lkey;

for (i = (llen * sizeof(__uint64_t)); i < (len - llen) ; i++)
buf[i] ^= (char)key;
asm:
4007f5: 48 8d 53 08 lea 0x8(%rbx),%rdx
4007f9: 31 c0 xor %eax,%eax
4007fb: 48 b9 0c 0c 0c 0c 0c mov $0xc0c0c0c0c0c0c0c,%rcx
400802: 0c 0c 0c
400805: 48 31 0b xor %rcx,(%rbx)
400808: 48 83 c0 01 add $0x1,%rax
40080c: 48 31 0a xor %rcx,(%rdx)
40080f: 48 3d 00 65 cd 1d cmp $0x1dcd6500,%rax
400815: 75 ee jne 400805 <test+0xa5>
400817: 31 f6 xor %esi,%esi
400819: 4c 89 e7 mov %r12,%rdi


Вариант 3, xor через 128 битныйу тип, SSE2 pxor инструкция + хвост по байтам:
__m128i *lbuf = (__m128i *)buf;
int llen = len % sizeof(__m128i);
for (i = 0; i < llen; i++)
lbuf[i] = __builtin_ia32_pxor128(lbuf[i], lkey);

for (i = (llen * sizeof( __m128i)); i < (len - llen) ; i++)
buf[i] ^= (char)key

asm:

400889: 66 0f 6f 0c 24 movdqa (%rsp),%xmm1
40088e: 31 d2 xor %edx,%edx
400890: 31 c0 xor %eax,%eax
400892: 66 0f 6f 04 18 movdqa (%rax,%rbx,1),%xmm0
400897: 66 0f ef c1 pxor %xmm1,%xmm0
40089b: 66 0f 7f 04 18 movdqa %xmm0,(%rax,%rbx,1)
4008a0: 48 83 c0 10 add $0x10,%rax
4008a4: 48 3d a0 00 00 00 cmp $0xa0,%rax
4008aa: 75 e6 jne 400892 <test+0x132>
4008ac: 48 83 c2 01 add $0x1,%rdx
4008b0: 48 81 fa 00 65 cd 1d cmp $0x1dcd6500,%rdx
4008b7: 75 d7 jne 400890 <test+0x130>
4008b9: 31 f6 xor %esi,%esi
4008bb: 4c 89 e7 mov %r12,%rdi


Теперь, самое интересное это циферки. Компилирую с -O2 и запускаю:

./a.out
test for i = 1000
duration 1 = 6665.891000
duration 2 = 2087.085000
duration 3 = 10276.917000
test for i = 100000
duration 1 = 6586.843000
duration 2 = 2090.889000
duration 3 = 10469.038000
test for i = 10000000
duration 1 = 6780.421000
duration 2 = 2082.808000
duration 3 = 10459.455000

i - размер буфера, duration - время для разных вариантов.

Теперь вопрос, это нормально что SSE2 инструкция работает медленнее, чем обычный xor на регистрах? Можно ли сделать xor быстрее?


Исходник теста положил сюда: http://codepaste.net/j1us7c






Offtop: Говорят, циклы с декрементом работают быстрее.

anon_666
()

Что-то как-то С не оптимально переводит в асм (может и исходник крив), но если поработать руками, имхо, можно порядком подсократить алгоритм. Напиши тоже самое на асме руками и сравни.

Ximen ★★★★
()

А кто RAND_bytes в исходники ложить будет?

Вощем собирал так:

gcc -O3 -funroll-loop test.c

Выхлоп теста:

 test for i = 1000
duration 1 = 63.398000
duration 2 = 85.329000
duration 3 = 7112.810000
 test for i = 100000
duration 1 = 56.890000
duration 2 = 85.336000
duration 3 = 8485.580000
 test for i = 10000000
duration 1 = 78.274000
duration 2 = 85.708000
duration 3 = 8758.358000

PS: Athlon 64 X2 4200+

AF ★★★
()
Ответ на: комментарий от sn1ln

Без -funroll-loops
./a.out
test for i = 1000
duration 1 = 7569.908000
duration 2 = 2070.178000
duration 3 = 10426.047000
test for i = 100000
duration 1 = 7617.404000
duration 2 = 2069.797000
duration 3 = 10413.663000
test for i = 10000000
duration 1 = 7520.315000
duration 2 = 2086.907000
duration 3 = 10471.985000

С -funroll-loops
./a.out
test for i = 1000
duration 1 = 9025.555000
duration 2 = 1999.697000
duration 3 = 2356.148000
test for i = 100000
duration 1 = 9027.357000
duration 2 = 2001.889000
duration 3 = 2360.036000
test for i = 10000000
duration 1 = 9048.975000
duration 2 = 2220.350000
duration 3 = 2481.062000

-funroll-loops подтянуло SSE2 вариант до приемлимового времени. И затормозило первый вариант. Будем разбиратся, спасибо.

Кстати -O2 у меня работает быстрее, чем -O3.

sn1ln
() автор топика

Посмотри у Агнера и на strchr

devl547 ★★★★★
()
Ответ на: комментарий от sn1ln

И это, в строке

int llen = len % sizeof(__uint64_t);

наверное, нужно деление, а не взятие остатка.

А в строке

xor_buf_key2(buf, 10, 0xc, lkey);

и ее аналогах неплохо было бы передавать актуалный размер массива )

AF ★★★
()
Ответ на: комментарий от maxxant

кажись выравнивание массива до 16 байт должно помочь

Если на 16 байт не выровнено (при сборке на 32-битном хосте, например), то movdqa экзепшн выкидывает.

mv ★★★★★
()
Ответ на: комментарий от sn1ln

-funroll-loops подтянуло SSE2 вариант до приемлимового времени.

Потому что xor ничего не стоит, а загрузка из памяти - очень даже. Поэтому компилятор с -O3 генерит код так, чтобы загрузка в следующий регистр и ксор с предыдущим чередовались. Так процессор может суперскалярность свою напрячь.

mv ★★★★★
()
Ответ на: комментарий от mv

mv логично, но тогда возникает вопрос для чего создавались SSE инструкции, если они не увиличивают скорость.

sn1ln
() автор топика
Ответ на: комментарий от sn1ln

Подправил тест (изначально он длину не правильную подставлял и он молотил по 10 байт только). Код тут:


http://codepaste.net/q3vpie Компилируем так gcc ./xor1.c -O2 -g -lssl -funroll-loops

Теперь да, SSE на больших буферах SSE выигрывает у регистров, но не намного:
test for i = 7000
duration 1 = 8007.167000
duration 2 = 292.539000
duration 3 = 242.309000

test for i = 9000
duration 1 = 10298.895000
duration 2 = 376.129000
duration 3 = 308.588000

Вообще я ожидал прироста где-то на 40%...

sn1ln
() автор топика

Ваша программа не компилируется, но мой вариант выдает duration = 6.218000 на 10000000 байтов на Celeron(R) Dual-Core CPU T3000 @ 1.80GHz в 32-битном режиме.

Dimanc ★★
()
Ответ на: комментарий от sn1ln

А без funroll-loops SSE ровно в 2 раза и выигрывает у регистров.

sn1ln
() автор топика
Ответ на: комментарий от Dimanc

Dimanc что gcc пишет у тебя? Так без данных сложно определит где ошибка. У меня все работает нормально.

Интересно можно ли функции сделать атрибут unroll-loops?

sn1ln
() автор топика
Ответ на: комментарий от sn1ln

xor.c:33:17: warning: return type defaults to ‘int’
xor.c: In function ‘xor_buf_key3’:
xor.c:40:25: warning: implicit declaration of function ‘__builtin_ia32_pxor128’
xor.c:40:33: error: incompatible types when assigning to type ‘__m128i’ from type ‘int’
xor.c: In function ‘test’:
xor.c:48:19: warning: unused variable ‘end’
xor.c:48:12: warning: unused variable ‘start’
xor.c: In function ‘main’:
xor.c:109:7: warning: pointer targets in passing argument 1 of ‘RAND_bytes’ differ in signedness
/usr/include/openssl/rand.h:102:6: note: expected ‘unsigned char *’ but argument is of type ‘char *’
xor.c:103:22: warning: unused variable ‘len’
xor.c:114:1: warning: control reaches end of non-void function
xor.c: In function ‘xor_buf_key3’:
xor.c:44:1: warning: control reaches end of non-void function

32 бит у меня.

Dimanc ★★
()
Ответ на: комментарий от sn1ln

mv логично, но тогда возникает вопрос для чего создавались SSE инструкции, если они не увиличивают скорость.

А они увеличивают. Попробуй написать конверсию цветовых пространств, и с SSE разницу в разы получишь. У меня экран из rgb16 в rgb32 в 4 с копейками раза быстрее с SSE2 разворачивается, и это учитывая, что данные тоже в кэш не помещаются, и многое упирается в паять. Или вот, например, сложение с сатурацией: в SSE это одна команда, да ещё и векторная, а в обычной логике вообще бранч (if) есть, что не есть гуд для процессора.

Просто в данном случае всё упирается в пропускную способность памяти, а обработка данных совсем простая, поэтому развернуться негде особо. Если вручную на ассемблере заоптимизировать оба варианта (uint64_t и SSE), то разницы в скорости между ними не будет.

mv ★★★★★
()

Про память тут дело говорят. Попробуй добавить ручной prefetch.

Yareg ★★★
()
Ответ на: комментарий от mv

>Если на 16 байт не выровнено (при сборке на 32-битном хосте, например), то movdqa экзепшн выкидывает.

А movdqu? Тут очень даже может быть невыровненная хрень, которая всё тормозит.

Yareg ★★★
()

>__m128i *lbuf = (__m128i *)buf;
алсо, говорят, что алиасить указатели — нехорошо...

Yareg ★★★
()
Ответ на: комментарий от anon_666

> Можно вручную цикл раскрутить.

В моем случае нельзя, так как размер буфера подаваемого на вход не известен.

sn1ln
() автор топика
Ответ на: комментарий от sn1ln

В вашем коде функции *xor*() получают в параметрах длину буфера, разве нет?

anon_666
()
Ответ на: комментарий от Yareg

А movdqu? Тут очень даже может быть невыровненная хрень, которая всё тормозит.

movdqu канает, но оно медленнее, чем movdqa. Хотя, ЕМНИП, на Нехалемах уже пофиг.

mv ★★★★★
()
Ответ на: комментарий от sn1ln

В моем случае нельзя, так как размер буфера подаваемого на вход не известен.

Крути блоками по 8-16 регистров, делов-то.

mv ★★★★★
()
Ответ на: комментарий от mv

Ну типа это.. Всем спасибо. Было довольно продуктивно.

sn1ln
() автор топика

У меня на говноатоме вроде адекватные результаты. Не делал только аналог второго варианта.

Вариант 1: http://codepaste.net/gq3ic3

Atom N270, gcc 4.5, O3:

Time with: 2360, time without: 11630, profit: 293%

Pentium III, icc 7.1, O3

Time with: 8340, time without: 12540, profit: nan%


Вариант 3: http://codepaste.net/g8xnnh

Atom N270, gcc 4.5, O3:

Time with: 2360, time without: 3890, profit: 65%

Pentium III, icc 7.1, O3:

Time with: 8360, time without: 6450, profit: nan%


//хз, почему на пентиуме наны, сейчас восемь часов утра, последний раз я проснулся сутки назад, а уснул полутора суток...
////указатели таки пришлось поалиасить АГРРРРРПРРРХХХХХ

Yareg ★★★
()
Ответ на: комментарий от mv

Да при размере буфера в сотни байт вполне можно раскрутить, как уже говорили, руками целиком, или хотя бы на 1-2к итераций, и в зависимости от размера буфера джампать в серёдку раскрученного цикла.

GAMer ★★★★★
()
Ответ на: комментарий от Dimanc

Yareg, спасибо очень интересно. Хорошую теорию по этой теме нашел тут http://lwn.net/Articles/255364/

Не понятно почему prefetch через блок, а не на следующий.

Скорее всего __m128i _mm_stream_load_si128 (__m128i *p); тоже делает prefetch перед следующей загрузкой. Надо будет глянуть.

sn1ln
() автор топика
Ответ на: комментарий от sn1ln

Ключик можно 1 раз грузить.


[root@max TEST]# ./xor128
Time with: 4120, time without: 13180, profit: 220%
[root@max TEST]# ./xor128_orig
Time with: 7570, time without: 13220, profit: 75%

--- xor128_orig.c   2011-01-13 11:01:38.000000000 -0800
+++ xor128.c   2011-01-13 11:02:03.000000000 -0800
@@ -63,10 +63,11 @@ static inline void do_with(unsigned char
{
unsigned long i;
__m128i a,c;
+
+ c=_mm_set1_epi32(key.key);
for(i=0;i<N;i+=16)
{
_mm_prefetch(&b[i+256],_MM_HINT_NTA);
- c=_mm_set1_epi32(key.key);
a=_mm_load_si128((__m128i *)&b[i]); //FFFFFFFFFFFFFFFFFFFFFFFFFFFUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUu-
a=_mm_xor_si128(a,c);
_mm_store_si128((__m128i *)&b[i],a);



sn1ln
() автор топика

Когда-то от нечего делать писал умножение матриц на SSE2 (ну расширение с 128-битными регистрами для плавающих чисел). Так вот - точно так же, вариант с SSE2 проигрывал float-варианту на сопроцессоре.

Процессор, на котором все проверялось, был, кажется, тоже от AMD.

amomymous ★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.