Enseignements en L3
Mention informatique

 


Vectorisation - Parcimonie

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 :


Algorithme de base / basic algorithm

int parsimony(char *vA, char *vB, char *vC, int len) {
  int i, f=0;

  for (i = 0; i < len; i++) {
    vC[i] = vA[i] & vB[i];
    if (vC[i] == 0) {
      vC[i] = vA[i] | vB[i];
      ++f;
    }
  }
  return f;
}

Amélioration : dépliage par 4 / Improvement : loop unrolling

int func_byte_unroll4(char *vA, char *vB, char *vC, int len) {
  int i, f=0;
  int len4 = (len & ~3);

  for (i = 0; i < len4; i+=4) {
    vC[i]   = vA[i] & vB[i];
    if (vC[i] == 0) {
      vC[i] = vA[i] | vB[i];
      ++f;
    }
    vC[i+1] = vA[i+1] & vB[i+1];
    if (vC[i+1] == 0) {
      vC[i+1] = vA[i+1] | vB[i+1];
      ++f;
    }
    vC[i+2] = vA[i+2] & vB[i+2];
    if (vC[i+2] == 0) {
      vC[i+2] = vA[i+2] | vB[i+2];
      ++f;
    }
    vC[i+3] = vA[i+3] & vB[i+3];
    if (vC[i+3] == 0) {
      vC[i+3] = vA[i+3] | vB[i+3];
      ++f;
    }
  }
  while (i < len) {
    vC[i] = vA[i] & vB[i];
    if (vC[i] == 0) {
      vC[i] = vA[i] | vB[i];
      ++f;
    }
    ++i;
  }
  return f;
}

Amélioration : vectorisation (version 1) / Improvement : vectorization

On choisit de stocker / we choose to store :
  • vA dans / in esi
  • vB dans / in edi
  • vC dans / in ebx
comportement des opérations sse sur les registres xmm /
behaviour of the sse operations over xmm registers

(les valeurs sont données en hexadécimal /
values are given in hexadecimal)
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]  ; vA
	mov	edi,[ebp+12] ; VB
	mov	ebx,[ebp+16] ; vC

	xor	eax,eax  ; fitch's score
        xor	ecx,ecx  ; i

	movdqu	xmm6,[one]

vect1_loop:
	movdqa	xmm0,[esi+ecx] ; xmm0 <- p (vA)

	movdqa	xmm2,[edi+ecx] ; xmm2 <- q (vB)
	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 ; vC

	; 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

  • PCMPEQB xmmDst,xmmSrc/m128
    (Packed compare if equal to byte)
    compare chacun des 16 octets des registres xmm. Si Src[i] == Dst[i] alors Dst[i] = 0xFF, sinon Dst[i] = 0x00. /
    compare each of the 16 bytes of xmm register. if Src[i] == Dst[i] then Dst[i] = 0xFF else Dst[i] = 0x00.
  • PMOVMSKB r(32/64), mmxSrc
    (Extract Packed Byte Mask)
    copie les bits de poids fort de chaque octet dans le registre spécifié. /
    copy the left most bit of each byte to the given register


Amélioration : vectorisation (version 2) / Improvement

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]  ; vA
	mov	edi,[ebp+12] ; vB
	mov	ebx,[ebp+16] ; vC

	xor	eax,eax
	xor	ecx,ecx

	movdqu	xmm6,[one]

vect2_loop:
	movdqa	xmm0,[esi+ecx] ; xmm0 <- p (vA)

	movdqa	xmm2,[edi+ecx] ; xmm2 <- q (vB)
	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 vC


	; 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) :

  • pand xmm1,xmm2 : 2 cycles
  • por xmm1,xmm2 : 2 cycles
  • pxor xmm1,xmm2 : 2 cycles
  • movdqa xmm1, mem128 : 2 cycles
  • pcmpeqb xmm1,xmm2 : 2 cycles
  • pmovmskb reg32,xmm1 : 3 cycles


Compter le nombre de bits à 1 : popcount

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
Temps en secondes pour 1000 itérations pour vérification 0 à 1.000.000
les sources ont été compilés uniquement avec l'option -O2 de gcc

  • Pentium II Celeron, 300 Mhz, 640 Mo SDRAM, 4 Go disque dur (Solo 2500, Gateway, 1999, under Windows 98)
  • Pentium III Tualatin, 1.4 Ghz, FSB 100/133 Mhz, 256 Mo SDRAM PC 100
  • Pentium-M, 2.0 Ghz, FSB 533 Mhz, 1 Go DDR-SDRAM
  • Pentium 4 Northwood, 2.8 Ghz, FSB 800 Mhz, 256 Mo DDR-SDRAM PC3200
  • Pentium D 830 Smithfield, 3 Ghz, FSB 800 Mhz, 1 Go DDR2-SDRAM PC4300
  • Core 2 Duo E6420, 2.13 Ghz, FSB 1066 Mhz, 2 Go DDR2-SDRAM PC 6400
  • Athlon 64 X2 4200+, 4.2 Ghz (2.2 Ghz), FSB 400 Mhz, 512 Mo DDR-SDRAM PC 3200
  • Sempron 3000+, FSB 400 Mhz, 512 Mo DDR-SDRAM PC 3200

Quelques commentaires :

  • une implantation naive peut parfois être grandement améliorée
  • les implantations les plus efficaces sont popcount3 ou 4
  • utiliser la division entière avec parcimonie sur les Athlon 64 / Sempron car elle est 3 fois plus pénalisante qu'une multiplication (cf. popcount7 / popcount8)


(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
  push   ebp
  xor    ecx,ecx
  mov    ebp,esp
  mov    edx,DWORD PTR [ebp+8]
  nop    
  lea    esi,[esi]
L1:
  movzx  eax,dl
  movsx  eax,BYTE PTR [eax+0x8048bd0]
  add    ecx,eax
  shr    edx,0x8
  jne    L1
  pop    ebp
  mov    eax,ecx
  ret    


popcount3_2
  push   ebp
  xor    ecx,ecx
  mov    ebp,esp
  mov    edx,DWORD PTR [ebp+8]


L1:
  movzx  eax,dl
  movsx  eax,BYTE PTR [eax+0x8048bc0]
  shr    edx,0x8
  add    ecx,eax
  test   edx,edx
  jne    L1
  pop    ebp
  mov    eax,ecx
  ret


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
push   ebp
mov    ebp,esp
push   ebx
mov    eax,DWORD PTR [ebp+8]
mov    ecx,eax
shr    ecx,1
and    ecx,0x55555555
and    eax,0x55555555
add    ecx,eax
mov    eax,ecx
shr    eax,0x2
and    eax,0x33333333
and    ecx,0x33333333
add    eax,ecx
mov    ecx,eax
shr    ecx,0x4
lea    ebx,[eax+ecx]
and    ebx,0xf0f0f0f
mov    edx,0x80808081
mov    eax,ebx
mul    edx
shr    edx,0x7
mov    ecx,edx
shl    ecx,0x8
sub    ecx,edx
sub    ebx,ecx
mov    eax,ebx
pop    ebx
pop    ebp
ret    
popcount2_2
push   ebp
mov    ebp,esp
mov    eax,DWORD PTR [ebp+8]
mov    edx,eax
shr    edx,1
and    edx,0x55555555
and    eax,0x55555555
add    edx,eax
mov    eax,edx
shr    eax,0x2
and    eax,0x33333333
and    edx,0x33333333
add    eax,edx
mov    edx,eax
shr    edx,0x4
add    eax,edx
and    eax,0xf0f0f0f
mov    edx,0xff
mov    ecx,edx
xor    edx,edx
div    ecx
mov    eax,edx
pop    ebp
ret    

Les latences des instructions div et mul sur Athlon 64 sont :

  • div mreg 16/32/64 : 23/39/71 cycles
  • mul mreg32 : 3 cycles

marqueur eStat\'Perso