Steigert das Ersetzen von "if"´s durch logische Konstrukte die Performance?



  • Hallo

    Am besten erkläre ich es an einem Beispiel, das zählt, wie oft ein Wert in einem Array vorkommt (mein performancekritisches Problem ist analog dazu)

    int func1(int* array, int number, int value){
      int number_of_value = 0;
      for(int i=0; i<number; i++){
        if(array[i] == value)
          number_of_value++;
      }
      return number_of_value;
    }
    
    int func2(int* array, int number, int value){
      int number_of_value = 0;
      for(int i = number-1; i >= 0; i--){              //i >= 0 sollte schneller als i<number sein
        number_of_value += (array[i] == value);        //Vermeidung des bedingten Sprunges
      }
      return number_of_value;
    }
    

    Es ist Code für einen normalen PC, also nicht embedded oder so.

    Ist func2 nennenswert schneller als func1? Kann allgemein gesagt werden, dass das Vermeiden von if´s schnelleren Code bringt? Welche anderen Optimierungsmaßnahmen gibt es sonst noch für dieses Beispiel?

    Würd mich freuen, wenn mir wer helfen könnte, meine Performancekenntnisse aufzubessern 😉



  • Nein, sollte so wie ich das sehe, keinen Boost geben (nicht keinen nennenswerten, sondern keinen). Denn nun muss der Bool'sche Wert ja in einen Integer umgewandelt werden.

    Zumal moderne Rechner solche Schleifen schon ziemlich flott können.

    Bist du mal mit einem Profiler an deinen Code gegangen und der hat dir gesagt, dass dort der Performanceflaschenhals liegt?



  • ok, danke.
    Mein Hintergedanke war, dass sich bedingte Sprünge und Pipelining nicht mögen

    Dann ist die Variante mit dem if besser, weil das sieht nicht so kryptisch aus 😉



  • Ich sag mal ganz pauschal: Dein Compiler ist klug genug, so was zu erkennen und zu optimieren. (Wenn du nicht irgendwas ganz komisches benutzt.) Wenn du die Optimierung ausschaltest, kann es vielleicht einen Unterschied machen, aber wen interessiert das?

    Die Argumentation mit der Umwandlung des boolschen Wertes in einen Integer kann ich aber nicht nachvollziehen. Der ist doch schon einer.



  • Der Code der da erzeugt wird ist durchaus unterschiedlich, ich habe mal folgende Funktionen zum Vergleichen genommen:

    std::uint64_t /*__declspec(noinline) */ count1(const std::int32_t* numbers, std::size_t size, std::int32_t number_to_count)
    {
    	std::uint64_t counter = 0;
    
    	for (std::size_t i = 0; i != size; ++i)
    	{
    		if (numbers[i] == number_to_count)
    			++counter;
    	}
    
    	return counter;
    }
    
    std::uint64_t /* __declspec(noinline) */ count2(const std::int32_t* numbers, std::size_t size, std::int32_t number_to_count)
    {
    	std::uint64_t counter = 0;
    
    	for (std::size_t i = 0; i != size; ++i)
    	{
    		counter += numbers[i] == number_to_count;
    	}
    
    	return counter;
    }
    

    GCC 4.9 -O3 -std=c++11 für count1:

    count1(int const*, unsigned long, int):
    	test	rsi, rsi
    	je	.L4
    	lea	rsi, [rdi+rsi*4]
    	xor	eax, eax
    .L3:
    	cmp	DWORD PTR [rdi], edx
    	lea	rcx, [rax+1]
    	cmove	rax, rcx
    	add	rdi, 4
    	cmp	rdi, rsi
    	jne	.L3
    	rep ret
    .L4:
    	xor	eax, eax
    	ret
    

    GCC 4.9 -O3 -std=c++11 für count2:

    count2(int const*, unsigned long, int):
    	test	rsi, rsi
    	push	rbx
    	je	.L13
    	mov	rcx, rdi
    	and	ecx, 15
    	shr	rcx, 2
    	neg	rcx
    	and	ecx, 3
    	cmp	rcx, rsi
    	cmova	rcx, rsi
    	cmp	rsi, 5
    	ja	.L25
    	mov	rcx, rsi
    .L3:
    	xor	eax, eax
    	cmp	DWORD PTR [rdi], edx
    	sete	al
    	cmp	rcx, 1
    	je	.L16
    	xor	r8d, r8d
    	cmp	DWORD PTR [rdi+4], edx
    	sete	r8b
    	add	rax, r8
    	cmp	rcx, 2
    	je	.L17
    	xor	r8d, r8d
    	cmp	DWORD PTR [rdi+8], edx
    	sete	r8b
    	add	rax, r8
    	cmp	rcx, 3
    	je	.L18
    	xor	r8d, r8d
    	cmp	DWORD PTR [rdi+12], edx
    	sete	r8b
    	add	rax, r8
    	cmp	rcx, 5
    	jne	.L19
    	xor	r8d, r8d
    	cmp	DWORD PTR [rdi+16], edx
    	sete	r8b
    	add	rax, r8
    	mov	r8d, 5
    .L5:
    	cmp	rsi, rcx
    	je	.L2
    .L4:
    	mov	r11, rsi
    	lea	r10, [rsi-1]
    	sub	r11, rcx
    	lea	r9, [r11-4]
    	sub	r10, rcx
    	shr	r9, 2
    	add	r9, 1
    	cmp	r10, 2
    	lea	rbx, [0+r9*4]
    	jbe	.L7
    	mov	DWORD PTR [rsp-4], edx
    	pxor	xmm2, xmm2
    	movd	xmm6, DWORD PTR [rsp-4]
    	pxor	xmm3, xmm3
    	movdqa	xmm4, XMMWORD PTR .LC0[rip]
    	lea	r10, [rdi+rcx*4]
    	pshufd	xmm5, xmm6, 0
    	xor	ecx, ecx
    .L8:
    	movdqa	xmm1, XMMWORD PTR [r10]
    	add	rcx, 1
    	add	r10, 16
    	cmp	r9, rcx
    	pcmpeqd	xmm1, xmm5
    	pand	xmm1, xmm4
    	movdqa	xmm0, xmm1
    	punpckldq	xmm1, xmm3
    	punpckhdq	xmm0, xmm3
    	paddq	xmm2, xmm1
    	paddq	xmm2, xmm0
    	ja	.L8
    	movdqa	xmm0, xmm2
    	add	r8, rbx
    	psrldq	xmm0, 8
    	paddq	xmm0, xmm2
    	movq	rcx, xmm0
    	add	rax, rcx
    	cmp	r11, rbx
    	je	.L2
    .L7:
    	xor	ecx, ecx
    	cmp	DWORD PTR [rdi+r8*4], edx
    	sete	cl
    	add	rax, rcx
    	lea	rcx, [r8+1]
    	cmp	rsi, rcx
    	je	.L2
    	cmp	DWORD PTR [rdi+rcx*4], edx
    	sete	cl
    	add	r8, 2
    	movzx	ecx, cl
    	add	rax, rcx
    	cmp	rsi, r8
    	je	.L2
    	cmp	DWORD PTR [rdi+r8*4], edx
    	sete	dl
    	movzx	edx, dl
    	add	rax, rdx
    .L2:
    	pop	rbx
    	ret
    .L25:
    	test	rcx, rcx
    	jne	.L3
    	xor	r8d, r8d
    	xor	eax, eax
    	jmp	.L4
    .L19:
    	mov	r8d, 4
    	jmp	.L5
    .L16:
    	mov	r8d, 1
    	jmp	.L5
    .L17:
    	mov	r8d, 2
    	jmp	.L5
    .L18:
    	mov	r8d, 3
    	jmp	.L5
    .L13:
    	xor	eax, eax
    	pop	rbx
    	ret
    .LC0:
    	.long	1
    	.long	1
    	.long	1
    	.long	1
    

    Also was der GCC sich dabei denkt muss mir auch mal jemand erklären. 🤡
    Bei VS gibt's auch Unterschiede:

    std::uint64_t __declspec(noinline) count1(const std::int32_t* numbers, std::size_t size, std::int32_t number_to_count)
    {
    002312A0  push        ebp  
    002312A1  mov         ebp,esp  
    002312A3  sub         esp,0Ch  
    002312A6  push        esi  
    002312A7  push        edi  
    002312A8  mov         edi,edx  
    002312AA  xorps       xmm0,xmm0  
    	std::uint64_t counter = 0;
    002312AD  movlpd      qword ptr [counter],xmm0  
    
    	for (std::size_t i = 0; i != size; ++i)
    002312B2  xor         esi,esi  
    002312B4  mov         edx,dword ptr [ebp-8]  
    002312B7  mov         eax,dword ptr [counter]  
    002312BA  test        edi,edi  
    002312BC  je          count1+31h (02312D1h)  
    002312BE  mov         edi,edi  
    	{
    		if (numbers[i] == number_to_count)
    002312C0  cmp         dword ptr [ecx+esi*4],2Ah  
    	{
    		if (numbers[i] == number_to_count)
    002312C4  jne         count1+2Ch (02312CCh)  
    			++counter;
    002312C6  add         eax,1  
    002312C9  adc         edx,0  
    
    	for (std::size_t i = 0; i != size; ++i)
    002312CC  inc         esi  
    002312CD  cmp         esi,edi  
    002312CF  jne         count1+20h (02312C0h)  
    	}
    
    	return counter;
    }
    002312D1  pop         edi  
    002312D2  pop         esi  
    002312D3  mov         esp,ebp  
    002312D5  pop         ebp  
    002312D6  ret
    
    std::uint64_t __declspec(noinline) count2(const std::int32_t* numbers, std::size_t size, std::int32_t number_to_count)
    {
    002312E0  push        ebp  
    002312E1  mov         ebp,esp  
    002312E3  sub         esp,18h  
    002312E6  push        ebx  
    002312E7  push        esi  
    002312E8  xorps       xmm0,xmm0  
    002312EB  mov         dword ptr [ebp-8],edx  
    	std::uint64_t counter = 0;
    
    	for (std::size_t i = 0; i != size; ++i)
    002312EE  xor         eax,eax  
    002312F0  movlpd      qword ptr [counter],xmm0  
    002312F5  mov         ebx,ecx  
    002312F7  push        edi  
    002312F8  test        edx,edx  
    002312FA  je          count2+54h (0231334h)  
    002312FC  mov         esi,dword ptr [ebp-10h]  
    	std::uint64_t counter = 0;
    
    	for (std::size_t i = 0; i != size; ++i)
    002312FF  mov         edi,dword ptr [counter]  
    	{
    		counter += numbers[i] == number_to_count;
    00231302  cmp         dword ptr [ebx+eax*4],2Ah  
    00231306  jne         count2+31h (0231311h)  
    00231308  mov         ecx,1  
    0023130D  xor         edx,edx  
    0023130F  jmp         count2+3Fh (023131Fh)  
    00231311  xorps       xmm0,xmm0  
    00231314  movlpd      qword ptr [counter],xmm0  
    00231319  mov         edx,dword ptr [ebp-10h]  
    0023131C  mov         ecx,dword ptr [counter]  
    0023131F  add         edi,ecx  
    00231321  adc         esi,edx  
    00231323  inc         eax  
    00231324  cmp         eax,dword ptr [ebp-8]  
    00231327  jne         count2+22h (0231302h)  
    	}
    
    	return counter;
    00231329  mov         eax,edi  
    0023132B  mov         edx,esi  
    }
    0023132D  pop         edi  
    0023132E  pop         esi  
    0023132F  pop         ebx  
    00231330  mov         esp,ebp  
    00231332  pop         ebp  
    00231333  ret  
    	}
    
    	return counter;
    00231334  mov         eax,dword ptr [counter]  
    00231337  mov         edx,dword ptr [ebp-10h]  
    }
    0023133A  pop         edi  
    0023133B  pop         esi  
    0023133C  pop         ebx  
    0023133D  mov         esp,ebp  
    0023133F  pop         ebp  
    00231340  ret
    

    clang 3.3 dagegen generiert bei beiden Funktionen exakt das gleiche: (sorry clang mag keine Intel-Syntax)

    .LCPI0_0:
    	.quad	4294967295              # 0xffffffff
    	.quad	4294967295              # 0xffffffff
    .LCPI0_1:
    	.quad	1                       # 0x1
    	.quad	1                       # 0x1
    count2(int const*, unsigned long, int):                         # @count2(int const*, unsigned long, int)
    	xorl	%eax, %eax
    	testq	%rsi, %rsi
    	je	.LBB0_8
    	movq	%rsi, %rcx
    	andq	$-4, %rcx
    	je	.LBB0_2
    	movd	%edx, %xmm2
    	movlhps	%xmm2, %xmm2            # xmm2 = xmm2[0,0]
    	pxor	%xmm0, %xmm0
    	xorl	%eax, %eax
    	movaps	.LCPI0_0(%rip), %xmm3
    	andps	%xmm3, %xmm2
    	movdqa	.LCPI0_1(%rip), %xmm4
    	pxor	%xmm1, %xmm1
    .LBB0_4:                                # %vector.body
    	movq	8(%rdi,%rax,4), %xmm5
    	pshufd	$16, %xmm5, %xmm6       # xmm6 = xmm5[0,0,1,0]
    	pand	%xmm3, %xmm6
    	pcmpeqd	%xmm2, %xmm6
    	pshufd	$-79, %xmm6, %xmm5      # xmm5 = xmm6[1,0,3,2]
    	movq	(%rdi,%rax,4), %xmm7
    	pand	%xmm6, %xmm5
    	pshufd	$16, %xmm7, %xmm6       # xmm6 = xmm7[0,0,1,0]
    	pand	%xmm3, %xmm6
    	pcmpeqd	%xmm2, %xmm6
    	addq	$4, %rax
    	pand	%xmm4, %xmm5
    	paddq	%xmm5, %xmm1
    	pshufd	$-79, %xmm6, %xmm5      # xmm5 = xmm6[1,0,3,2]
    	pand	%xmm6, %xmm5
    	pand	%xmm4, %xmm5
    	paddq	%xmm5, %xmm0
    	cmpq	%rax, %rcx
    	jne	.LBB0_4
    	jmp	.LBB0_5
    .LBB0_2:
    	xorl	%ecx, %ecx
    	pxor	%xmm0, %xmm0
    	pxor	%xmm1, %xmm1
    .LBB0_5:                                # %middle.block
    	paddq	%xmm0, %xmm1
    	movdqa	%xmm1, %xmm0
    	punpckhqdq	%xmm0, %xmm0    # xmm0 = xmm0[1,1]
    	paddq	%xmm1, %xmm0
    	movd	%xmm0, %rax
    	cmpq	%rsi, %rcx
    	je	.LBB0_8
    	subq	%rcx, %rsi
    	leaq	(%rdi,%rcx,4), %r8
    .LBB0_7:                                # %scalar.ph
    	cmpl	%edx, (%r8)
    	sete	%cl
    	movzbl	%cl, %edi
    	addq	%rdi, %rax
    	addq	$4, %r8
    	decq	%rsi
    	jne	.LBB0_7
    .LBB0_8:                                # %._crit_edge
    	ret
    

  • Mod

    cooky451 schrieb:

    Also was der GCC sich dabei denkt muss mir auch mal jemand erklären.

    Er vektorisiert und betrachtet 4 Zahlen gleichzeitig.



  • Oh. Ich muss alles zurücknehmen und bin ziemlich überrascht, dass das dabei herauskommt. Auch deshalb, weil gcc 4.8 mit -O2 noch exakt den gleichen Code für beide Varianten erzeugt (mit -O3 dann auch nicht mehr).

    Das ist schon seltsam, dass er für die erste Variante nicht mehr die selbe Struktur erkennt. Scheinbar läuft er irgendwo vorher in einen anderen Optimierungspfad -- ersetzt die Verzweigung durch den lea/cmove-Trick -- und findet von da aus die Vektorisierung nicht mehr. Spannend.


  • Mod

    Ja, Vektorisierung ist ab -O3 standardmäßig aktiviert.


Anmelden zum Antworten