Stel, ik heb een struct zoals deze:
Op een typische 32-bits architectuur is dat ding natuurlijk precies zo groot als een int. Als ik twee variabelen heb van dit type, en de ene aan de andere toeken, dan zal de compiler ook gewoon netjes een enkele instructie genereren waarbij in één keer een 32-bits waarde wordt toegekend.
Helaas is voor structs geen gelijkheidsoperator gedefinieert. Ik vraag me dan dus af op welke manier ik twee structs op net zo'n efficiënte manier zou kunnen vergelijken. De meest voor de hand liggende optie is dit:
Dit werkt, maar genereert ook echt code voor vier vergelijkingen, met allerlei bitshifts en conditionele jumps en andere toestanden. Veel te veel code voor wat feitelijk een word-sized comparison is, naar mijn idee. Blijkbaar heeft de compiler niet door dat ik álle velden aan het vergelijken ben en dat dat in één keer kan gebeuren.
Stel dat ik even gebruik maak van het feit dat ik weet dat de struct geen padding bevat (wat de compiler ook weet) en ik ze dus met memcmp() kan vergelijken:
Als dit een function call zou worden zou het natuurlijk nog steeds niet efficiënt zijn, maar dit is een standard library functie die de compiler kent en speciaal optimaliseert. Er wordt code gegenereerd van de vorm:
Met nog het nodige gedoe eromheen om een stackframe te bouwen en registers te saven/restoren. De compiler heeft de call naar memcmp() vervangen door een rep cmpsb constructie, wat waarschijnlijk beter is dan een reeks vergelijkingen, maar het is nog steeds belachelijk veel overhead en een impliciete loop. Waarschijnlijk komt dat doordat de memcmp() meer doet dan ik wil, want het is een comperator die een lexicografische vergelijking uitvoert. De compiler beseft niet dat ik het resultaat uiteindelijk alleen maar met 0 vergelijk.
Dus ik zoek nog even verder en er blijkt ook nog een (deprecated) functie bcmp() te bestaan, wat precies specificieert wat ik zoek: het werkt als memcmp() behalve dat de functie nul of niet-nul retourneert afhankelijk van of de buffers wel of niet gelijk zijn. Aan een niet-nul resultaat wordt verder geen enkele betekenis toegekend. Dat moet toch aanzienlijk beter te optimaliseren zijn, leek me! Helaas genereert de compiler hier precies dezelfde code, waarschijnlijk omdat het een deprecated functie is waarvoor dus niet specifiek geoptimaliseerd wordt.
Gelukkig heb ik ook wel wat methoden gevonden die wél naar wens werken, als ik ook gebruik dat ik toevallig weet dat de struct even groot is als een int. Ik kan ze dan via via naar een int casten:
Dit werkt correct, hoewel het dubieus op me over kwam in combinatie met strict aliasing regels. (Is dit eigenlijk een overtreding of niet?)
Een variant die zeker veilig is, is het gebruik van een union:
Niet de meest mooie code. In C99 kan ik zelfs een union constructor gebruiken, wat misschien iets prettiger leest, maar dus niet echt compatible is met oudere C standaarden (wat natuurlijk jammer is):
Alledrie de varianten compileren naar dezelfde, efficiënte code die ik graag in het begin gezien had:
(De enige reden dat het nog twee in plaats van één instructie kost, is dat beide argumenten op de stack staan in plaats van in registers.)
Helaas generaliseert deze aanpak slecht naar meerdere platformen met wisselende groottes van datatypes, complexere structures, en dergelijke. Ik zou liever gezien hebben dat de compiler op de een of andere manier bedenkt op welke manier twee structures inhoudelijk het beste vergeleken kunnen worden en daar zélf de beste code bij genereert.
De vraag die ik hierbij wil stellen is de volgende: herkennen anderen dit probleem, en zo ja, hoe lossen jullie het op?
C:
1
| struct s { char a, b, c, d; }; |
Op een typische 32-bits architectuur is dat ding natuurlijk precies zo groot als een int. Als ik twee variabelen heb van dit type, en de ene aan de andere toeken, dan zal de compiler ook gewoon netjes een enkele instructie genereren waarbij in één keer een 32-bits waarde wordt toegekend.
Helaas is voor structs geen gelijkheidsoperator gedefinieert. Ik vraag me dan dus af op welke manier ik twee structs op net zo'n efficiënte manier zou kunnen vergelijken. De meest voor de hand liggende optie is dit:
C:
1
2
3
| int equal(struct s x, struct s y) { return x.a == y.a && x.b == y.b && x.c == y.c && x.d == y.d; } |
Dit werkt, maar genereert ook echt code voor vier vergelijkingen, met allerlei bitshifts en conditionele jumps en andere toestanden. Veel te veel code voor wat feitelijk een word-sized comparison is, naar mijn idee. Blijkbaar heeft de compiler niet door dat ik álle velden aan het vergelijken ben en dat dat in één keer kan gebeuren.
Stel dat ik even gebruik maak van het feit dat ik weet dat de struct geen padding bevat (wat de compiler ook weet) en ik ze dus met memcmp() kan vergelijken:
C:
1
2
3
4
| struct s { char a, b, c, d; }; int equal(struct s x, struct s y) { return memcmp(&x, &y, sizeof(struct s)) == 0; } |
Als dit een function call zou worden zou het natuurlijk nog steeds niet efficiënt zijn, maar dit is een standard library functie die de compiler kent en speciaal optimaliseert. Er wordt code gegenereerd van de vorm:
code:
1
2
3
4
5
6
7
| cld movl $4, %ecx leal 12(%ebp), %edi leal 8(%ebp), %esi repz cmpsb movl (%esp), %esi movl 4(%esp), %edi |
Met nog het nodige gedoe eromheen om een stackframe te bouwen en registers te saven/restoren. De compiler heeft de call naar memcmp() vervangen door een rep cmpsb constructie, wat waarschijnlijk beter is dan een reeks vergelijkingen, maar het is nog steeds belachelijk veel overhead en een impliciete loop. Waarschijnlijk komt dat doordat de memcmp() meer doet dan ik wil, want het is een comperator die een lexicografische vergelijking uitvoert. De compiler beseft niet dat ik het resultaat uiteindelijk alleen maar met 0 vergelijk.
Dus ik zoek nog even verder en er blijkt ook nog een (deprecated) functie bcmp() te bestaan, wat precies specificieert wat ik zoek: het werkt als memcmp() behalve dat de functie nul of niet-nul retourneert afhankelijk van of de buffers wel of niet gelijk zijn. Aan een niet-nul resultaat wordt verder geen enkele betekenis toegekend. Dat moet toch aanzienlijk beter te optimaliseren zijn, leek me! Helaas genereert de compiler hier precies dezelfde code, waarschijnlijk omdat het een deprecated functie is waarvoor dus niet specifiek geoptimaliseerd wordt.
Gelukkig heb ik ook wel wat methoden gevonden die wél naar wens werken, als ik ook gebruik dat ik toevallig weet dat de struct even groot is als een int. Ik kan ze dan via via naar een int casten:
C:
1
2
3
| int equal(struct s x, struct s y) { return *(int*)&x == *(int*)&y; } |
Dit werkt correct, hoewel het dubieus op me over kwam in combinatie met strict aliasing regels. (Is dit eigenlijk een overtreding of niet?)
Een variant die zeker veilig is, is het gebruik van een union:
C:
1
2
3
4
| int equal(struct s x, struct s y) { union u { struct s s; int i; } ux = { x }, uy = { y }; return ux.i == uy.i; } |
Niet de meest mooie code. In C99 kan ik zelfs een union constructor gebruiken, wat misschien iets prettiger leest, maar dus niet echt compatible is met oudere C standaarden (wat natuurlijk jammer is):
C:
1
2
3
4
| int equal(struct s x, struct s y) { union u { struct s s; int i; }; return ((union u)x).i == ((union u)y).i; } |
Alledrie de varianten compileren naar dezelfde, efficiënte code die ik graag in het begin gezien had:
C:
1
2
| movl 12(%ebp), %eax cmpl %eax, 8(%ebp) |
(De enige reden dat het nog twee in plaats van één instructie kost, is dat beide argumenten op de stack staan in plaats van in registers.)
Helaas generaliseert deze aanpak slecht naar meerdere platformen met wisselende groottes van datatypes, complexere structures, en dergelijke. Ik zou liever gezien hebben dat de compiler op de een of andere manier bedenkt op welke manier twee structures inhoudelijk het beste vergeleken kunnen worden en daar zélf de beste code bij genereert.
De vraag die ik hierbij wil stellen is de volgende: herkennen anderen dit probleem, en zo ja, hoe lossen jullie het op?