This page shows how to optimize the computation of fitch's score when dealing with maximum parsimony. We use the SSE instructions implemented on Intel or AMD processors to speed up the computations (see this page for time improvement for different architectures).
source code is available :
int parsimony(char *x, char *y, char *z, int size) {
int i, changes=0;
for (i = 0; i < size; i++) {
z[i] = x[i] & y[i];
if (z[i] == 0) {
z[i] = x[i] | y[i];
++changes;
}
}
return changes;
}
int parsimony(char *x, char *y, char *z, int n) {
int i, changes=0;
for (i=0; i < size; ++i) {
char x_, y_;
int c;
x_ = x[i] & y[i];
y_ = x[i] | y[i];
z[i] = x_ | ((!x_) * y_);
c = ((!x_) & 1);
changes += c;
}
return changes;
}
int parsimony(char *x, char *y, char *z, int n) {
int i, changes=0;
for (i=0; i < size; ++i) {
char x_, y_;
int c;
x_ = x[i] & y[i];
y_ = x[i] | y[i];
z[i] = (x_ == 0) ? y_ : x_; // change here !
c = ((!x_) & 1);
changes += c;
}
return changes;
}
registre / register | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
p | 02 | 10 | 08 | 02 | 20 | 02 | 10 | 01 | 08 | 02 | 04 | 02 | 04 | 02 | 20 | 10 |
q | 01 | 01 | 02 | 10 | 02 | 20 | 08 | 20 | 08 | 10 | 10 | 02 | 20 | 02 | 01 | 04 |
p & q | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 08 | 00 | 00 | 02 | 00 | 02 | 00 | 00 |
p | q | 03 | 11 | 0A | 12 | 22 | 22 | 18 | 21 | 08 | 12 | 14 | 02 | 24 | 02 | 21 | 14 |
pcmpeqb xmm4,xmm3 | FF | FF | FF | FF | FF | FF | FF | FF | 00 | FF | FF | 00 | FF | 00 | FF | FF |
pmovmskb edx,xmm4 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 |
not | 0000.0000b = 00h = 0 #=0 | 1001.0100b = 94h = 148 #=3 | ||||||||||||||
1111.1111b = FFh = 255 | 0110.1011b = D6h = 214 |
section .data
; 128 bits set to 1, this value will be used to get a
; complementary value
align 16
one: dd 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF
section .code
;
; only valid for 16x data
;
func_byte_vect1_16x:
push ebp
mov ebp,esp
push esi
push edi
push ebx
push ecx
push edx
mov esi,[ebp+8] ; x
mov edi,[ebp+12] ; y
mov ebx,[ebp+16] ; z
xor eax,eax ; fitch's score
xor ecx,ecx ; i
movdqu xmm6,[one]
vect1_loop:
movdqa xmm0,[esi+ecx] ; xmm0 <- p (x)
movdqa xmm2,[edi+ecx] ; xmm2 <- q (y)
movdqa xmm1,xmm0 ; xmm1 <- p
pxor xmm3,xmm3
pxor xmm5,xmm5
pand xmm0,xmm2 ; xmm0 <- p & q
por xmm1,xmm2 ; xmm1 <- p | q
movdqa xmm4,xmm0 ; xmm4 <- p & q
pcmpeqb xmm4,xmm3 ; xmm4[i] ?= 0 for all i
pmovmskb edx,xmm4
movdqa xmm5,xmm6 ; xmm5 = 0xFF...F
pxor xmm5,xmm4 ; xmm5 = ~xmm4
pand xmm1,xmm4
pand xmm0,xmm5
por xmm0,xmm1
movdqa [ebx+ecx],xmm0 ; z
; eax contains fitch score
; compute number of bits set to 1
; bt will set carry flag to 1 if bit is set
bt edx,0
adc eax,0
bt edx,1
adc eax,0
bt edx,2
adc eax,0
bt edx,3
adc eax,0
bt edx,4
adc eax,0
bt edx,5
adc eax,0
bt edx,6
adc eax,0
bt edx,7
adc eax,0
bt edx,8
adc eax,0
bt edx,9
adc eax,0
bt edx,10
adc eax,0
bt edx,11
adc eax,0
bt edx,12
adc eax,0
bt edx,13
adc eax,0
bt edx,14
adc eax,0
bt edx,15
adc eax,0
add ecx,16
cmp ecx,[ebp+20]
jl vect1_loop
pop edx
pop ecx
pop ebx
pop edi
pop esi
mov esp,ebp
pop ebp
ret
La partie qui pose le plus de problème concerne le calcul du nombre de bits à 1 dans le registre edx après exécution de l'instruction pmovmskb (cf. ci-après popcount = population count). L'utilisation de l'instruction bt associée à adc ralentit considérablement les calculs. On envisage donc d'utiliser une table de conversion.
The most difficult problem is how can we find the number of bits set to 1 in register edx (see popcount below = population count). The instruction pair bt and adc is not efficient enough, so we decide to use a conversion table.
;
; only valid for 16x data
;
func_byte_vect2_16x:
push ebp
mov ebp,esp
push esi
push edi
push ebx
push ecx
push edx
mov esi,[ebp+8] ; x
mov edi,[ebp+12] ; y
mov ebx,[ebp+16] ; z
xor eax,eax
xor ecx,ecx
movdqu xmm6,[one]
vect2_loop:
movdqa xmm0,[esi+ecx] ; xmm0 <- p (x)
movdqa xmm2,[edi+ecx] ; xmm2 <- q (y)
movdqa xmm1,xmm0 ; xmm1 <- p
pxor xmm3,xmm3
pxor xmm5,xmm5
pand xmm0,xmm2 ; xmm0 <- p & q
por xmm1,xmm2 ; xmm1 <- p | q
movdqa xmm4,xmm0 ; xmm4 <- p & q
pcmpeqb xmm4,xmm3 ; xmm4[i]
pmovmskb edx,xmm4
movdqa xmm5,xmm6
pxor xmm5,xmm4 ; xmm5 = ~xmm4
pand xmm1,xmm4
pand xmm0,xmm5
por xmm0,xmm1
movdqa [ebx+ecx],xmm0 ; put result in z
; improvement to compute number of bits set to 1 in edx
; use a table to convert dl (and then dh) into number of
; bits
push ebx ; temporary use of ebx, so save ebx in stack
mov ebx,edx
xor edx,edx
mov dl,bl ; get first lower 8 bits
mov dl,[translation_table+edx]
; note : we could use movzx edx,byte [translation_table+edx]
; but it seems slower than the mov dl,[translation_table+edx]
; on Core 2 Duo architecture
add eax,edx
mov dl,bh ; get upper 8 bits
mov dl,[translation_table+edx]
add eax,edx
pop ebx
add ecx,16
cmp ecx,[ebp+20]
jl vect2_loop
pop edx
pop ecx
pop ebx
pop edi
pop esi
mov esp,ebp
pop ebp
ret
translation_table:
db 0, 1, 1, 2, 1, 2, 2, 3
db 1, 2, 2, 3, 2, 3, 3, 4
db 1, 2, 2, 3, 2, 3, 3, 4
db 2, 3, 3, 4, 3, 4, 4, 5
db 1, 2, 2, 3, 2, 3, 3, 4
db 2, 3, 3, 4, 3, 4, 4, 5
db 2, 3, 3, 4, 3, 4, 4, 5
db 3, 4, 4, 5, 4, 5, 5, 6
db 1, 2, 2, 3, 2, 3, 3, 4
db 2, 3, 3, 4, 3, 4, 4, 5
db 2, 3, 3, 4, 3, 4, 4, 5
db 3, 4, 4, 5, 4, 5, 5, 6
db 2, 3, 3, 4, 3, 4, 4, 5
db 3, 4, 4, 5, 4, 5, 5, 6
db 3, 4, 4, 5, 4, 5, 5, 6
db 4, 5, 5, 6, 5, 6, 6, 7
db 1, 2, 2, 3, 2, 3, 3, 4
db 2, 3, 3, 4, 3, 4, 4, 5
db 2, 3, 3, 4, 3, 4, 4, 5
db 3, 4, 4, 5, 4, 5, 5, 6
db 2, 3, 3, 4, 3, 4, 4, 5
db 3, 4, 4, 5, 4, 5, 5, 6
db 3, 4, 4, 5, 4, 5, 5, 6
db 4, 5, 5, 6, 5, 6, 6, 7
db 2, 3, 3, 4, 3, 4, 4, 5
db 3, 4, 4, 5, 4, 5, 5, 6
db 3, 4, 4, 5, 4, 5, 5, 6
db 4, 5, 5, 6, 5, 6, 6, 7
db 3, 4, 4, 5, 4, 5, 5, 6
db 4, 5, 5, 6, 5, 6, 6, 7
db 4, 5, 5, 6, 5, 6, 6, 7
db 5, 6, 6, 7, 6, 7, 7, 8
Quelques latences d'instructions (source Athlon 64 X2) :
méthode | temps (s) | gain |
C (basique) | 43.090 | - |
C (optimisé 1) | 39.770 | 7% |
C (optimisé 2) | 29.310 | 32% |
assembleur 1 | 25.360 | 41% |
assembleur 2 | 21.750 | 51% |
assembleur 3 | 21.420 | 51% |
SSE | 2.230 | 95% |
assembleur 1, 2 et 3 sont des versions assembleur des versions optimisées
méthode | temps (s) |
version 1 | 54.520 |
version 2 | 48.590 |
version 3 | 45.640 |
version 4 (optimized) | 44.600 |
version 5 (optimized AVX) | 45.190 |
La fonction qui consiste à compter le nombre de bits à 1 stockés dans un mot (WORD : 16 bits) ou un double mot (DWORD : 32 bits) s'appelle popcount. Elle n'a été implantée nativement que sur certains processeurs et peut être étendue à 64 ou 128 bits.
Elle devrait normalement être implantée au niveau des instructions SSSE4 (Supplementary SSE3 = SSE4) chez Intel à partir du Core 2 Nehalem (fin 2008) et sera identifiée par le mnémonique POPCNT.
On peut cependant implanter en C ou en assembleur cette fonction. Voici quelques exemples d'implantations trouvés sur le net. Note : this code was found on internet.
/* very naive : student's style ;-) */
int popcount0(unsigned long x) {
int n=0;
while (x) {
if ((x&1)) ++n;
x = (x >> 1);
}
return n;
}
/* basic implementation */
int popcount1(unsigned long x) {
int n = 0;
if (x) do ++n; while ((x &= x-1)!=0);
return n;
}
/* optimized version for all kind of architectures */
int popcount2(unsigned long x) {
x = ((x>>1) & 0x55555555) + (x & 0x55555555);
x = ((x>>2) & 0x33333333) + (x & 0x33333333);
return (((x>>4)+x) & 0x0F0F0F0F) % 255;
}
/* use of a conversion table */
int popcount3(unsigned long x) {
int n = 0;
do n += "\0\1\1\2\1\2\2\3\1\2\2\3\2\3\3\4" \
"\1\2\2\3\2\3\3\4\2\3\3\4\3\4\4\5" \
"\1\2\2\3\2\3\3\4\2\3\3\4\3\4\4\5" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\1\2\2\3\2\3\3\4\2\3\3\4\3\4\4\5" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\3\4\4\5\4\5\5\6\4\5\5\6\5\6\6\7" \
"\1\2\2\3\2\3\3\4\2\3\3\4\3\4\4\5" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\3\4\4\5\4\5\5\6\4\5\5\6\5\6\6\7" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\3\4\4\5\4\5\5\6\4\5\5\6\5\6\6\7" \
"\3\4\4\5\4\5\5\6\4\5\5\6\5\6\6\7" \
"\4\5\5\6\5\6\6\7\5\6\6\7\6\7\7\10" [x & 0xFF];
while ((x >>= 8)!=0);
return n;
}
/* improvement of popcount3 */
int popcount4(unsigned long x) {
static char __attribute__((aligned(16))) *table4 =
"\0\1\1\2\1\2\2\3\1\2\2\3\2\3\3\4" \
"\1\2\2\3\2\3\3\4\2\3\3\4\3\4\4\5" \
"\1\2\2\3\2\3\3\4\2\3\3\4\3\4\4\5" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\1\2\2\3\2\3\3\4\2\3\3\4\3\4\4\5" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\3\4\4\5\4\5\5\6\4\5\5\6\5\6\6\7" \
"\1\2\2\3\2\3\3\4\2\3\3\4\3\4\4\5" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\3\4\4\5\4\5\5\6\4\5\5\6\5\6\6\7" \
"\2\3\3\4\3\4\4\5\3\4\4\5\4\5\5\6" \
"\3\4\4\5\4\5\5\6\4\5\5\6\5\6\6\7" \
"\3\4\4\5\4\5\5\6\4\5\5\6\5\6\6\7" \
"\4\5\5\6\5\6\6\7\5\6\6\7\6\7\7\10" ;
int n = 0;
n = table4[x & 0xFF];
x >>= 8;
n += table4[x & 0xFF];
x >>= 8;
n += table4[x & 0xFF];
x >>= 8;
n += table4[x & 0xFF];
return n;
}
Amélioration | Pentium-M | Pentium III | Pentium 4 | Pentium D | C2D E6420 | Athlon 64 | Sempron |
popcount 0 | 44 | 45.66 | 42.87 | 30.41 | 30.27 | 38.78 | |
popcount 1 | 25 | 21.18 | 23.10 | 19.06 | 22.08 | 28.63 | |
popcount 2 | 12 | 20.502 | 9.76 | 8.08 | 9.51 | 12.33 | |
popcount 3 | 71 | 9.76 | 8.12 | 5.27 | 7.16 | 9.35 | |
popcount 4 (/popcount0) |
6 (-86%) |
8.68 (-81%) |
7.78 (-81%) |
5.29 (-82%) |
6.38 (-78%) |
8.27 (-78%) |
|
popcount 5 (popcount3_1) |
21 | 10.07 | 8.08 | 21.27 | 7.14 | 9.31 | |
popcount 6 (popcount3_2) |
7 | 9.92 | 8.23 | 6.02 | 7.16 | 9.28 | |
popcount 7 (popcount2_1) mul |
12 | 13.86 | 10.04 | 9.48 | 9.06 | 11.74 | |
popcount 8 (popcount2_2) div |
17 | 20.47 | 20.73 | 12.93 | 25.81 | 33.46 | |
POPCNT | NA | NA | NA | NA | NA | NA | NA |
Amélioration | Pentium II | P9500 |
popcount 0 | 320 | 23.81 |
popcount 1 | 170 | 15.51 |
popcount 2 | 103 | 7.25 |
popcount 3 | 156 | 4.20 |
popcount 4 | 62 (-80%) |
4.70 (-80%) |
popcount 5 | 156 | 4.86 |
popcount 6 | 75 | 4.86 |
popcount 7 | 101 | 7.43 |
popcount 8 | 160 | 6.58 |
Quelques commentaires :
(1) Note : en compilant sous Ubuntu Linux avec gcc version 4.1.2 avec un Pentium-M, je me suis aperçu que si on utilisait les options -march=pentium-m -mtune=pentium-m on obtenait pour popcount 3 un temps d'exécution de 23s, alors qu'en supprimant ces options, on obtient un temps d'exécution de 8s !! Voici le code de chaque sous-programme (objdump -d -j .text -M intel popcount.exe) :
23s -march=pentium-m | 8s |
popcount3_1
|
popcount3_2
|
Restait à déterminer si les 2 instructions nop et lea étaient pénalisantes ou si les instructions à l'intérieur de la boucle L1 influençaient le temps d'exécution. J'ai donc réécrit popcount3_1 en assembleur en supprimant les 2 instructions étranges (en fait elles permettent de réaliser l'alignement des instructions de la boucle L1).
Supprimer les 2 instructions permet de gagner moins d'une seconde. En fait, l'introduction d'une instruction test edx,edx ou cmp edx,0 avant l'instruction jne L1 permet de diminuer le temps d'exécution à 8s ! Pretty strange isn't it !
Ce phénomène apparaît sur Pentium-M 760 et Core 2 Duo E6420, mais pas sur Pentium 4 ou Athlon 64. Il s'agit en fait d'un EFLAGS partial register stall (cf chapitre 6). Il s'agit probablement d'un bug corrigé depuis, car il ne se reproduit pas sur un Core 2 Duo E6750.
(2) sur Pentium 4 avec gcc 4.0.2, en fonction que l'on utilise ou pas l'option -march=pentium4, on obtient une version de popcount2 avec multiplication (13s) ou division (20s) .
13s | 20s |
popcount2_1
|
popcount2_2
|
Les latences des instructions div et mul sur Athlon 64 sont :