Vad är snabbaste sättet att köra vektormultiplikation med XOR-operatören?

Permalänk

Vad är snabbaste sättet att köra vektormultiplikation med XOR-operatören?

Antar att du har en matris A som har M rader och N kolumner.
Matrisen A har datatypen osignerad 8-bit, dvs uint8_t.

Vi tar en vektor x som har dimensionen N (längd N) och multiplicerar den med matrisen A så kommer vi få vektorn y.

y = A*x

Men här har jag tänkt att använda mig av XOR. Detta betyder att om y är 0, så kommer vektorn x vara identisk en viss rad på matrisen A.

y = A^x

Frågor:
För att få detta så optimalt som möjligt, så bör matrisen A ha så stor datatyp som möjligt, t.ex. uint64_t eller högre, om det finns.

1. Har alla moderna C kompilatorer stöd för uint64_t eller högre?
2. Vad händer om en hårdvara har inte stöd för 64-bit?
3. Har ARM stöd för 64-bit? Lite mera specifika processorer: Cortex-M3 och Cortex-M4
4. Vad är det absolut snabbaste sättet att utföra XOR multiplikation?

Vi kan ta ett exempel där vi multiplicerar en rad i A med vektorn x

#include <stdio.h> typedef unsigned long long uint64_t; typedef uint64_t size_t; #define N 4 int main(){ /* Skapa en rad i matrisen A */ uint64_t A[N] = {255, 65535, 511, 1023}; /* Skapa vektor */ uint64_t x[N] = {255, 60000, 510, 1000}; /* Iterera array */ size_t i; for(i = 0; i < N; i++){ printf("%i\n", (int) A[i] ^ x[i]); } return 0; }

Utskriften blev:

0 5535 1 23

Men är detta optimalt att göra så?

För i praktiken så ska jag jämföra talen med varandra.

#include <stdio.h> typedef unsigned long long uint64_t; typedef uint64_t size_t; #define N 4 int main(){ /* Skapa en rad i matrisen A */ uint64_t A[N] = {255, 65535, 511, 1023}; /* Skapa vektor */ uint64_t x[N] = {255, 65535, 511, 1023}; /* Iterera array */ size_t i, j; for(i = 0; i < N; i++){ for(j = 0; j < N; j++){ printf("%i\t", (int) A[i] ^ x[j]); } printf("\n"); } return 0; }

Utskrift:

0 65280 256 768 65280 0 65024 64512 256 65024 0 512 768 64512 512 0

Syftet är att veta vid vilket index är ett element i vektorn x likadant som ett element i matrisen A.
Men detta är O(N^2) komplexibilitet. Om jag ska testa flera rader i matrisen A, dvs om raderna M togs med i uppgiften, så kommer jag få tre stycken for-satser, vilket är O(N^3) komplexibilitet och nu börjar vi tala om oeffektivt.

Så vad är snabbaste sättet?

Permalänk
Medlem

Du kan själv se vilken assemblerkod du får ut på https://godbolt.org/

ARM64 gcc 13.2.0 (-O2) ger följande för din loop:

lsl x1, x19, 3
add x2, x21, x1
add x1, x22, x1
add x19, x19, 1
mov x0, x20
ldr x2, [x2, -8]
ldrsw x1, [x1, -8]
eor x1, x1, x2
bl printf
cmp x19, 5
bne .L2

ARM GCC 13.2.0 (-O2) ger följande:
ldr r3, [r5], #8
ldr r2, [r4]
ldr r1, [r4, #4]
eor r2, r2, r3
mov r0, r7
eor r3, r1, r3, asr #31
bl printf
cmp r5, r8
add r4, r4, #8
bne .L2

Så det blir 2 stycken XOR på "32-bitars" Arm.

Permalänk
Skrivet av Perkoff:

Du kan själv se vilken assemblerkod du får ut på https://godbolt.org/

ARM64 gcc 13.2.0 (-O2) ger följande för din loop:

lsl x1, x19, 3
add x2, x21, x1
add x1, x22, x1
add x19, x19, 1
mov x0, x20
ldr x2, [x2, -8]
ldrsw x1, [x1, -8]
eor x1, x1, x2
bl printf
cmp x19, 5
bne .L2

ARM GCC 13.2.0 (-O2) ger följande:
ldr r3, [r5], #8
ldr r2, [r4]
ldr r1, [r4, #4]
eor r2, r2, r3
mov r0, r7
eor r3, r1, r3, asr #31
bl printf
cmp r5, r8
add r4, r4, #8
bne .L2

Så det blir 2 stycken XOR på "32-bitars" Arm.

Men finns det inget t.ex. BLAS eller MLK som kan göra detta åt mig? Iterationer är väldigt långsamma.

Permalänk
Medlem
Skrivet av heretic16:

Frågor:
För att få detta så optimalt som möjligt, så bör matrisen A ha så stor datatyp som möjligt, t.ex. uint64_t eller högre, om det finns.

1. Har alla moderna C kompilatorer stöd för uint64_t eller högre?
2. Vad händer om en hårdvara har inte stöd för 64-bit?
3. Har ARM stöd för 64-bit? Lite mera specifika processorer: Cortex-M3 och Cortex-M4
4. Vad är det absolut snabbaste sättet att utföra XOR multiplikation?

1. I praktiken har alla moderna C kompilatorer du kommer att komma i kontakt med stöd för uint64_t, men i teorin garanteras egentligen bara att stöd för (unsigned) long long finns, som måste vara minst 64 bitar bred.
Rent generellt så skall man aldrig använda datatyper med exakt storlek (såsom int8_t eller uint32_t) när man programmerar C om det inte är viktigt att datatypen har just exakt den storleken och ingen annan, eftersom det inte finns någon garanti att de datatyperna stöds av alla C kompilatorer.

2. Har hårdvaran inte stöd för 64-bitars tal så går det lite långsammare att göra uträkningar med 64-bitars tal eftersom det behövs fler instruktioner per beräkning då, men att göra det är inget problem

3. Beror på vilken version och implementation av ARM. För just de processorerna så orkar jag inte googla åt dig. Det bör du klara själv.

4. Jag är inte riktigt säker på att jag förstår vad du menar med "XOR multiplikation" eller vad du tror dig ha för nytta av det.

Permalänk
Medlem
Skrivet av Erik_T:

4. Jag är inte riktigt säker på att jag förstår vad du menar med "XOR multiplikation" eller vad du tror dig ha för nytta av det.

Jo, det skulle vara väldigt intressant att veta vad det är för algoritm som behöver köra XOR mellan en vektor och varje rad i en matris...

Eller för den delen hur stora matriser som ska bearbetas...

Permalänk
Skrivet av Erik_T:

1. I praktiken har alla moderna C kompilatorer du kommer att komma i kontakt med stöd för uint64_t, men i teorin garanteras egentligen bara att stöd för (unsigned) long long finns, som måste vara minst 64 bitar bred.
Rent generellt så skall man aldrig använda datatyper med exakt storlek (såsom int8_t eller uint32_t) när man programmerar C om det inte är viktigt att datatypen har just exakt den storleken och ingen annan, eftersom det inte finns någon garanti att de datatyperna stöds av alla C kompilatorer.

2. Har hårdvaran inte stöd för 64-bitars tal så går det lite långsammare att göra uträkningar med 64-bitars tal eftersom det behövs fler instruktioner per beräkning då, men att göra det är inget problem

3. Beror på vilken version och implementation av ARM. För just de processorerna så orkar jag inte googla åt dig. Det bör du klara själv.

4. Jag är inte riktigt säker på att jag förstår vad du menar med "XOR multiplikation" eller vad du tror dig ha för nytta av det.

1. Jag brukar använda size_t typ när jag inte vet storleken. Jag håller på mycket med matrisalgebra och mycket data med C. Jag jobbar sällan med negativa tal.

2. Okej. Antar att man kan använda uint64_t på en Windows 95 dator, fast det går lite långsamare. Viktigaste för mig är att det fungerar. Just nu har jag valt uint32_t som min maximala storlek.

4. Jag håller på med bildanalys.
2:30 in i filmen så förklarar han: https://www.youtube.com/watch?v=25GkgxClSaU

Med XOR så kan du utföra K-nearest neighborg med Hamming distance.

Permalänk
Medlem
Skrivet av heretic16:

1. Jag brukar använda size_t typ när jag inte vet storleken. Jag håller på mycket med matrisalgebra och mycket data med C. Jag jobbar sällan med negativa tal.

Vet man inte riktigt hur stora heltal man behöver så rekommenderas normalt att man använder int/unsigned int. Det är normalt den "naturliga" heltalsstorleken för arkitekturen.

Alla olika '_t' typedefs är till för specifika användningsområden, inte när man bara behöver ett vanligt heltal.

Citat:

2. Okej. Antar att man kan använda uint64_t på en Windows 95 dator, fast det går lite långsamare. Viktigaste för mig är att det fungerar. Just nu har jag valt uint32_t som min maximala storlek.

Så länge du har en kompilator som stöder det så kan du använda uint64_t på en jäkla brödrost!
Hårdvara och OS har i princip ingen betydelse alls för vilka datatyper som kan användas. Hårdvaran påverkar däremot vilka datatyper som kompilatortillverkaren väljer att stödja.

Permalänk
Skrivet av Erik_T:

Vet man inte riktigt hur stora heltal man behöver så rekommenderas normalt att man använder int/unsigned int. Det är normalt den "naturliga" heltalsstorleken för arkitekturen.

Alla olika '_t' typedefs är till för specifika användningsområden, inte när man bara behöver ett vanligt heltal.

Så länge du har en kompilator som stöder det så kan du använda uint64_t på en jäkla brödrost!
Hårdvara och OS har i princip ingen betydelse alls för vilka datatyper som kan användas. Hårdvaran påverkar däremot vilka datatyper som kompilatortillverkaren väljer att stödja.

Visst kan en kompilator optimera om man t.ex. har valt uint32_t, men arrayens siffror är 8-bit bara?

Okej, då vet jag.
Men då återgår vi till frågan: Finns det något smart sätt att göra vektormultiplikation med XOR?

Permalänk
Medlem
Skrivet av heretic16:

Visst kan en kompilator optimera om man t.ex. har valt uint32_t, men arrayens siffror är 8-bit bara?

Okej, då vet jag.
Men då återgår vi till frågan: Finns det något smart sätt att göra vektormultiplikation med XOR?

Har du sagt att elementen i arrayen är uint32_t så kommer kompilatorn att använda exakt 32 bitar för varje element, oavsett vilka värden som lagras i den.

Berätta först vad du menar med "vektormultiplikation med XOR" så skall vi se om jag (eller någon annan här) har något svar på om de kan göras smart.

Permalänk
Skrivet av Erik_T:

Har du sagt att elementen i arrayen är uint32_t så kommer kompilatorn att använda exakt 32 bitar för varje element, oavsett vilka värden som lagras i den.

Berätta först vad du menar med "vektormultiplikation med XOR" så skall vi se om jag (eller någon annan här) har något svar på om de kan göras smart.

Jo, Men tänk om du har en matris som ser ut så här

0b01010100101110101010 0b10101010111010010000 0b11010101001010100111 0b01010101111111001000 0b11111101111011001111 0b01000011000011101000 0b01011010111011010011

Du får en vektor som ser ut så här:

0b01010011010011101000

Om du multiplicerar denna vektor med matrisen med XOR

0b01010100101110101010 ^ 0b01010011010011101000 = 0b0000111111101000010 0b10101010111010010000 ^ 0b01010011010011101000 = 0b11111001101001111000 0b11010101001010100111 ^ 0b01010011010011101000 = 0b10000110011001001111 0b01010101111111001000 ^ 0b01010011010011101000 = 0b0000110101100100000 0b11111101111011001111 ^ 0b01010011010011101000 = 0b10101110101000100111 0b01000011000011101000 ^ 0b01010011010011101000 = 0b0010000010000000000 0b01011010111011010011 ^ 0b01010011010011101000 = 0b0001001101000111011

Så kommer du märka att en rad i denna matris kommer att ha många nollor och få 1:or. Om du summerar antalet 1:or så kommer du märka följande att den näst sista raden som gav resultatet

0b0010000010000000000

Den har bara antalet 1:or som två stycken. Alltså betyder detta att denna vektor

0b01010011010011101000

och denna rad i matrisen

0b01000011000011101000

Är väldigt likadana.

Detta kallas för K-nearest neighbor med Hamming Distance. Den används för att t.ex. kunna klassificera deskriptörer i bilder....eller jämföra olika DNA med varandra.

Nackdelen med K-nearest neighbor är att man måste spara denna matris i minnet. Alternativt kan man bygga ett neuralt nätverk av allt, vilket fungerar också.

Permalänk
Medlem
Skrivet av heretic16:

Men finns det inget t.ex. BLAS eller MLK som kan göra detta åt mig? Iterationer är väldigt långsamma.

ARM Cortex-M3 har inga vektoroperationer:
https://www.ic.unicamp.br/~celio/mc404-s2-2015/docs/manual-de...

Registren som du arbetar med är 32 bitar stora för EOR.

Att iterera här är inte speciellt långsamt, eftersom du accessar elementen i arrayen respektive matrisen sekvensiellt.

En motsvarighet till BLAS och MLK kan inte "trolla fram" en snabbare implementation än att du kör EOR på varje element...

Permalänk
Hedersmedlem
Skrivet av Perkoff:

En motsvarighet till BLAS och MLK kan inte "trolla fram" en snabbare implementation än att du kör EOR på varje element...

Även om det kanske inte är aktuellt för just de där processorerna är väl annars vektoroperationer ett sätt att ”trolla”? Man vill nog försöka använda sådana om det finns stöd på systemet man kör.

Permalänk
Medlem
Skrivet av Elgot:

Även om det kanske inte är aktuellt för just de där processorerna är väl annars vektoroperationer ett sätt att ”trolla”? Man vill nog försöka använda sådana om det finns stöd på systemet man kör.

Exempel på hur man kan få en modern kompilator att använda SIMD på bitwise-operationer:
https://stackoverflow.com/questions/15067119/how-can-i-use-si...

unsigned char * restrict p1 = __builtin_assume_aligned(r1, 16);
unsigned char * restrict p2 = __builtin_assume_aligned(r2, 16);
for (unsigned int i = 0; i < len; ++i) { p2[i] = p1[i] ^ p2[i]; }

Detta resulterar i nästan optimal kod som utnyttjar de "bredaste" tillgängliga registren för processorarkitekturen som är target. Men likväl är det loopar man arbetar med...