Forum: PC-Programmierung Intrinsic Funktion _bittest langsamer?


von Christian (Gast)


Lesenswert?

Hallo.
Wollte mal testen inwieweit intrinsische Funktionen für 
Geschwindigkeit-optimierte Programme einsetzbar sind.

Zum Vergleich habe ich die intr. Funktion _bittest() eingesetzt und
anschließend im normalen C-Code umgesetzt.

Das Ergebnis war überraschend, da _bittest() um mehrere msec langsamer
ist als die normale Bitprüfung in C.

Wie kann das sein?
Der Compiler sollte doch _bittest entsprechend optimieren können.


Hier der Programmausschnitt:

int nbit,i;
long num = 2147483648;


Code Intrinsic:
---------------------------------------
...
for(i = 0; i < 80000; i++)
  for(nbit = 0; nbit < 32; nbit++)
 {
   test[nbit] = _bittest(&num,nbit);
 }
...
---------------------------------------

normales c:
---------------------------------------
...
for(a = 0; a < 80000; a++)
  for(nbit = 0; nbit < 32; nbit++)
  {
    if( num & (1<<nbit))
      test[nbit]=1;
    else
      test[nbit]=0;
  }
...
---------------------------------------
Umgebung:
Win Vista,Dual Core,Visual Studio 2010

Danke.

Gruß
Christian

von Robert L. (lrlr)


Lesenswert?

asm code schon angeschaut?

von Christian (Gast)


Lesenswert?

Habe den Assemblercode noch nicht angesehen.
Wollte erstmal sehen ob mein Compiler _bittest() entsprechend optimieren 
kann und ob das effizienter ist als normales Bit prüfen.

Christian

von Peter II (Gast)


Lesenswert?

ist überhaupt die optimierung eingeschaltet? Dann müsste er doch sogar 
die äußere schleife komplett entfernen, weil sie keinen sinn macht.

von Christian (Gast)


Lesenswert?

Die Optimierung ist eingeschaltet(Geschwindigkeit maximieren /O2).

Die äußere Schleife dient nur dazu das Bit- testen x-mal zu wiederholen.
Ist die äußere Schleife durchlaufen, kann ich dann in milli- Sekunden
messen(GetLocalTime() kann nicht besser auflösen als 1ms).


Christian

von Kan a. (Firma: Basta) (kanasta)


Lesenswert?

Es heisst "-O2".

von Christian (Gast)


Lesenswert?

Nein. Richtig ist /O2.

Siehe: [[http://msdn.microsoft.com/en-us/library/8f8h5cxt.aspx]]

Christian

von Kan a. (Firma: Basta) (kanasta)


Lesenswert?

Ok, Standard ist gcc, deshalb habe ich es mal angenommen, weil du keinen 
Comnpiler nennst. Was ist mit dem Assembler-Code?

von Peter II (Gast)


Lesenswert?

Christian schrieb:
> Die äußere Schleife dient nur dazu das Bit- testen x-mal zu wiederholen.

das ist schon klar, aber der optimierer kann sie entfernen weil sie für 
ihn kein sinn macht und der egebniss am ende das gleiche ist.

von Peter II (Gast)


Lesenswert?

liese mal ob du nicht noch andere schalter setzen musst

http://msdn.microsoft.com/en-us/library/26td21ds%28v=vs.80%29.aspx

von Christian (Gast)


Lesenswert?

Hier mal den Assemblercode für Bitset():

test[nbit] = _bittest(&num,nbit);
01201072  lea         ecx,[ebp-7Ch]
01201075  bt          dword ptr [ecx],eax
01201078  inc         eax
01201079  cmp         eax,20h
0120107C  jl          wmain+72h (1201072h)

Sieht nicht unbedingt optimiert aus.

Christian

von Christian (Gast)


Lesenswert?

Der Schalter für /Oi (Generate Intrinsic Functions) ist auch gesetzt.

Trotzdem produziert _bittest() langsameren Programmcode!

Christian

von Fabian O. (xfr)


Lesenswert?

Hast Du die Variable num als volatile deklariert? Wenn nicht, wird wie 
gesagt wahrscheinlich die äußere Schleife wegoptimiert.

von Robert L. (lrlr)


Lesenswert?

>wegoptimiert

wäre auch mein erster verdacht gewesen, aber wie soll der compiler das 
erkennen?

von Christian (Gast)


Lesenswert?

>Hast Du die Variable num als volatile deklariert?

Hab ich. Keine Veränderung.

Christian

von Kan a. (Firma: Basta) (kanasta)


Lesenswert?

Christian schrieb:
> Hier mal den Assemblercode für Bitset():
>
> test[nbit] = _bittest(&num,nbit);
> 01201072  lea         ecx,[ebp-7Ch]
> 01201075  bt          dword ptr [ecx],eax
> 01201078  inc         eax
> 01201079  cmp         eax,20h
> 0120107C  jl          wmain+72h (1201072h)
>
> Sieht nicht unbedingt optimiert aus.
>
> Christian

Und wie sieht der Assembler-Code zur C-Bittest-Funktion aus? Es wäre 
nicht schlecht, beides zu posten, wenn es um den Vergleich geht...

von Robert L. (lrlr)


Lesenswert?

vermutlich lassen sich bei der Version mit shl und cmp
die Sprünge besser vorhersehen und/oder einige Instruktionen parallel 
bearbeiten
also (auch wenn es mehr sind)  schneller abarbeiten..



ps

---------------------------------------
...
for(a = 0; a < 80000; a++)
  for(nbit = 0; nbit < 32; nbit++)
  {
    if( _bittest(&num,nbit))
      test[nbit]=1;
    else
      test[nbit]=0;


hast das schon mal probiert? nur so interessehalber (nicht weil es 
sinnvoll wäre)

von Christian (Gast)


Lesenswert?

Hier mal das Assembly vom C Code:

00E91030  test        ecx,80000000h
00E91036  setne       dl
00E91039  mov         byte ptr [ebp+eax-24h],dl
00E9103D  inc         eax
00E9103E  rol         ecx,1
00E91040  cmp         eax,20h
00E91043  jl          wmain+30h (0E91030h)

Warum dieser asm Code schneller ist als der asm Code von _bittest()
erschließt sich mir leider nicht.

Christian

von Robert L. (lrlr)


Lesenswert?

>erschließt sich mir leider nicht.

weil dort überhaupt kein "bittest" mehr drinnen ist

der ganze teil wurde wegoptimiert (weil test[] nach der schleife NIE 
verwende wird...

von Kan a. (Firma: Basta) (kanasta)


Lesenswert?

Ich erkenne da schon noch einen Bittest, mit test ecx und rol ecx.
Die lea-Instruktion im ersten asm-Auszug benötigt viel Zeit.

von Christian (Gast)


Lesenswert?

>der ganze teil wurde wegoptimiert

Habe mal das array test[] auf volatile und auch nach der Schleife mal 
mit Dummy Werten beschrieben.Der Compiler sollte nichts mehr 
wegoptimieren. Das Ergebnis ist dasselbe.

_bittest() benötigt im Schnitt 10ms, ein normaler Bittest(ohne 
_bittets()) benötigt im Schnitt 5ms.

Also doppelt so schnell wenn man keine Intrinsic einsetzt.

Christian

von Kan a. (Firma: Basta) (kanasta)


Lesenswert?

Das ganze hängt stark von Prozessortyp, Cache und Speichermodell ab.
Grundsätzlich steht intrinsic function für eine in eine Funktion 
eingebettete Operation. Danach sieht der Assembler-Auszug nicht aus. 
Christian, hast du beim intrinsic-Asm-Code evtl. zu viel weggeschnitten?

Zwar enthält die Intrinsic-Version in der Schleife weniger 
Instruktionen, trotzdem benötigt lea viel Zeit, und du hast einen 
Datenkonflikt in eax, was zu einem Pipeline-Stall führt, was zusätzlich 
Zeit kostet.

Christian schrieb:
> Wollte mal testen inwieweit intrinsische Funktionen für
> Geschwindigkeit-optimierte Programme einsetzbar sind.

Fazit: Die Anwendung von intrinsischen Funktionen macht nur in 
Spezialfällen Sinn. Aus meiner Sicht ist das ganze Experiment stark 
sinnfrei.

von Christian (Gast)


Lesenswert?

>Christian, hast du beim intrinsic-Asm-Code evtl. zu viel weggeschnitten?

Hab ich.Aber nur denn Teil mit den Schleifen.

>und du hast einen Datenkonflikt in eax

Woran erkennst du das?

> Aus meiner Sicht ist das ganze Experiment stark sinnfrei.

Wollte nur mal wissen welche Auswirkungen _bittest() auf mein Programm 
hat hinsichtlich Geschwindigkeit.Wenn der Compiler so leistungsfähige 
Optionen bietet,warum dann nicht verwenden.Daher der Test.Nur das 
Ergebnis ist ernüchternd.

Christian

von Robert L. (lrlr)


Lesenswert?

>Ich erkenne da schon noch einen Bittest, mit test ecx

das ist die Schleife (8000 in oct ??)

von Kan a. (Firma: Basta) (kanasta)


Lesenswert?

Per Definition hat Intrinsic nichts mit leistungsfähig zu tun, eher
im Gegenteil.

Was Datenkonflikte sind lernt man in Rechnerarchitektur, da findest du 
bestimmt auch zahlreiche Infos im Internet. Im C-Code erkenne ich zw. 
setne und mov einen RAW(Read after Write)-Konflikt mit dl. In der 
intrinsischen Variante finde ich zw. lea und bt, bzgl. ecx und zw. inc 
und cmp bzgl. eax einen Konflikt.

von Kan a. (Firma: Basta) (kanasta)


Lesenswert?

Christian schrieb:

> 00E91030  test        ecx,80000000h
> 00E91036  setne       dl
> 00E91039  mov         byte ptr [ebp+eax-24h],dl
> 00E9103D  inc         eax
> 00E9103E  rol         ecx,1
> 00E91040  cmp         eax,20h
> 00E91043  jl          wmain+30h (0E91030h)

für Robert:

test ecx, 8... testet das höchste Bit in ecx
Das Ergebnis landet in dl, und dann im Speicher (00E91036, 00E91039)
eax ist die Schleifen-Variable und wird hochgezählt
ecx enthält das zu prüfende Wort und wird um eine Stelle nach links 
geshiftet. eax wird mit 32 verglichen, danach kommt die Schleife...
P.S. Kein Asm-Dump enthält oct...

von Robert L. (lrlr)


Lesenswert?

(hab ich ja ziemlich Blödsinn gepostet..)

unglaublich dass das so optimiert wird..

von Christian (Gast)


Lesenswert?

>Im C-Code erkenne ich zw. setne und mov einen RAW(Read after Write)-Konflikt mit 
dl.

Sorry, aber auch wenn ich die Vorlesung Rechner- Architektur nicht 
besucht
habe, bin ich mir sicher, das man aus den asm Ausschnitten oben, keine
Konflikte ableiten kann.

Weil 1.
Assembler Code aus rein sequenziellen Ausführungseinheiten besteht.
Der RAW-Konflikt macht sich aber erst in der massiven parallel 
ausgeführten Pipeline bemerkbar. Ohne Kenntnisse der verwendeten 
CPU/Pipeline Einheit und des eingesetzten Compilers solch eine Aussage 
zutreffen halte ich für gewagt.

2.
Es sind keine Umordnungsbefehle oder Verzögerungselemente (_NOP) im asm 
Code.

3.
Prozessoren heutzutage Datenkonflikte in Hardware lösen(Data Forwarding 
Unit).

Gruß
Christian

von Christian (Gast)


Lesenswert?

>Per Definition hat Intrinsic nichts mit leistungsfähig zu tun

Meinte damit die Leistungsfähigkeit eines Compilers, bestimmte 
Funktionen optimierend auszuführen.

Christian

von Arc N. (arc)


Lesenswert?

Christian schrieb:
>>Per Definition hat Intrinsic nichts mit leistungsfähig zu tun
>
> Meinte damit die Leistungsfähigkeit eines Compilers, bestimmte
> Funktionen optimierend auszuführen.
>
> Christian

Auch die optimierte Variante ist langsamer:
1
int bitTestStandard(const long* array, long bitNumber, size_t len) {
2
    int numberOfSetBits = 0;
3
    for (size_t i = 0; i < len; i++) {
4
        numberOfSetBits += (*array & (1 << bitNumber)) ? 1 : 0;
5
        array++;
6
    }
7
    return numberOfSetBits;
8
}
9
10
int bitTestIntrinsic(const long* array, long bitNumber, size_t len) { 
11
    int numberOfSetBits = 0;
12
    for (size_t i = 0; i < len; i++) {
13
        numberOfSetBits += _bittest(array, bitNumber) ? 1 : 0;
14
        array++;
15
    }
16
    return numberOfSetBits;
17
}
18
19
const int testArraySize = 123456789;
20
const long testArrayBitNumber = 13;
21
long *testArray;
22
23
int _tmain(int argc, _TCHAR* argv[]) {
24
    testArray = new long[testArraySize];
25
26
    for (size_t i = 0; i < testArraySize; i++) {
27
        testArray[i] = i;
28
    }  
29
30
    auto tickCountIntrinsic = GetTickCount64();
31
    auto resultIntrinsic = bitTestIntrinsic(testArray, testArrayBitNumber, testArraySize);
32
    tickCountIntrinsic = GetTickCount64() - tickCountIntrinsic;
33
34
    auto tickCountStandard = GetTickCount64();
35
    auto resultStandard = bitTestStandard(testArray, testArrayBitNumber, testArraySize);
36
    tickCountStandard = GetTickCount64() - tickCountStandard;
37
38
    printf("Intrinsic: %lld, %d, Standard: %lld, %d\n",  tickCountIntrinsic, resultIntrinsic, tickCountStandard, resultStandard);
39
40
    return 0;
41
}

VC2012
/Ox (full optimization)
/Ob2 (inline any suitable)
/Oi (enable intrinsics)
/Ot (favor fast code)
/Oy (omit frame pointers)
/GL (whole program optimization)
/arch:AVX
1
  testArray = new long[testArraySize];
2
00C8100D  push        1D6F3454h  
3
00C81012  call        dword ptr ds:[0C82094h]  
4
00C81018  movdqa      xmm1,xmmword ptr ds:[0C82130h]  
5
00C81020  add         esp,4  
6
00C81023  mov         dword ptr ds:[00C83368h],eax  
7
8
  for (size_t i = 0; i < testArraySize; i++) {
9
00C81028  xor         ecx,ecx  
10
00C8102A  mov         edx,eax  
11
00C8102C  lea         esp,[esp]  
12
00C81030  movd        xmm0,ecx  
13
00C81034  pshufd      xmm0,xmm0,0  
14
00C81039  paddd       xmm0,xmm1  
15
00C8103D  add         ecx,4  
16
    testArray[i] = i;
17
00C81040  movdqu      xmmword ptr [edx],xmm0  
18
00C81044  lea         edx,[edx+10h]  
19
00C81047  cmp         ecx,75BCD14h  
20
00C8104D  jb          wmain+30h (0C81030h)  
21
22
  for (size_t i = 0; i < testArraySize; i++) {
23
00C8104F  cmp         ecx,75BCD15h  
24
00C81055  jae         wmain+6Ch (0C8106Ch)  
25
00C81057  jmp         wmain+60h (0C81060h)  
26
00C81059  lea         esp,[esp]  
27
    testArray[i] = i;
28
00C81060  mov         dword ptr [eax+ecx*4],ecx  
29
00C81063  inc         ecx  
30
00C81064  cmp         ecx,75BCD15h  
31
00C8106A  jb          wmain+60h (0C81060h)  
32
  }  
33
34
  auto tickCountIntrinsic = GetTickCount64();
35
00C8106C  mov         ebx,dword ptr ds:[0C82000h]  
36
00C81072  call        ebx  
37
  auto resultIntrinsic = bitTestIntrinsic(testArray, testArrayBitNumber, testArraySize);
38
00C81074  mov         ecx,dword ptr ds:[0C83368h]  
39
  auto resultIntrinsic = bitTestIntrinsic(testArray, testArrayBitNumber, testArraySize);
40
00C8107A  mov         ebp,eax  
41
00C8107C  mov         dword ptr [esp+10h],edx  
42
00C81080  xor         edi,edi  
43
  }  
44
45
  auto tickCountIntrinsic = GetTickCount64();
46
00C81082  mov         esi,75BCD15h  
47
00C81087  jmp         wmain+90h (0C81090h)  
48
00C81089  lea         esp,[esp]  
49
  auto resultIntrinsic = bitTestIntrinsic(testArray, testArrayBitNumber, testArraySize);
50
00C81090  bt          dword ptr [ecx],0Dh  
51
00C81094  mov         eax,0  
52
00C81099  setb        al  
53
00C8109C  lea         ecx,[ecx+4]  
54
00C8109F  add         edi,eax  
55
00C810A1  dec         esi  
56
00C810A2  jne         wmain+90h (0C81090h)  
57
  tickCountIntrinsic = GetTickCount64() - tickCountIntrinsic;
58
00C810A4  call        ebx  
59
00C810A6  sub         eax,ebp  
60
00C810A8  mov         ecx,edx  
61
00C810AA  sbb         ecx,dword ptr [esp+10h]  
62
00C810AE  mov         dword ptr [esp+1Ch],eax  
63
00C810B2  mov         dword ptr [esp+18h],ecx  
64
65
  auto tickCountStandard = GetTickCount64();
66
00C810B6  call        ebx  
67
00C810B8  mov         ecx,dword ptr ds:[0C83368h]  
68
00C810BE  mov         dword ptr [esp+14h],edx  
69
00C810C2  xor         edx,edx  
70
00C810C4  xor         ebx,ebx  
71
00C810C6  mov         dword ptr [esp+10h],eax  
72
00C810CA  add         ecx,8  
73
00C810CD  mov         ebp,273EF07h  
74
  auto resultStandard = bitTestStandard(testArray, testArrayBitNumber, testArraySize);
75
00C810D2  mov         eax,dword ptr [ecx-8]  
76
00C810D5  shr         eax,0Dh  
77
  auto resultStandard = bitTestStandard(testArray, testArrayBitNumber, testArraySize);
78
00C810D8  and         eax,1  
79
00C810DB  add         esi,eax  
80
00C810DD  mov         eax,dword ptr [ecx-4]  
81
00C810E0  shr         eax,0Dh  
82
00C810E3  and         eax,1  
83
00C810E6  add         ebx,eax  
84
00C810E8  mov         eax,dword ptr [ecx]  
85
00C810EA  shr         eax,0Dh  
86
00C810ED  and         eax,1  
87
00C810F0  add         edx,eax  
88
00C810F2  lea         ecx,[ecx+0Ch]  
89
00C810F5  dec         ebp  
90
00C810F6  jne         wmain+0D2h (0C810D2h)  
91
  tickCountStandard = GetTickCount64() - tickCountStandard;
92
00C810F8  lea         eax,[edx+ebx]  
93
00C810FB  add         esi,eax  
94
00C810FD  call        dword ptr ds:[0C82000h]  
95
00C81103  sub         eax,dword ptr [esp+10h]

von (prx) A. K. (prx)


Lesenswert?

_bittest steht nicht für die schnellstmögliche Operation, Bits in Worten 
zu testen, sondern direkt für den Befehl BT. Und der ist bei variabler 
Bitnummer auf Speicher ausgesprochen komplex, weil nicht auf 0..31 oder 
0..63 begrenzt.

Die Bitnummer wird vom Prozessor per unoptimiertem Microcode erst in 
eine Nummer innerhalb des Wortes und einen Offset auf die Adresse 
umgerechnet. Folglich ist diese Variante von BT ausgesprochen langsam, 
bei leidlich aktuellen Intels beispielsweise ~10 Takte.

In realem Code wird man nicht selten feststellen, dass ein Compiler 
solchen Code besser optimiert, als die Hardware ihn per Microcode 
ausführt. Solche Effekte führten mit zur Entwicklung von RISC 
Architekturen.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Das meinen die GCC-Entwickler dazu:
1
;; %%% bts, btr, btc, bt.
2
;; In general these instructions are *slow* when applied to memory,
3
;; since they enforce atomic operation.  When applied to registers,
4
;; it depends on the cpu implementation.  They're never faster than
5
;; the corresponding and/ior/xor operations, so with 32-bit there's
6
;; no point.  But in 64-bit, we can't hold the relevant immediates
7
;; within the instruction itself, so operating on bits in the high
8
;; 32-bits of a register becomes easier.
9
;;
10
;; These are slow on Nocona, but fast on Athlon64.  We do require the use
11
;; of btrq and btcq for corner cases of post-reload expansion of absdf and
12
;; negdf respectively, so they can never be disabled entirely.

(aus ggg-4.7.2/gcc/config/i386/i386.md)

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.