web-dev-qa-db-fra.com

Quelle est la division entière la plus rapide qui prend en charge la division par zéro, quel que soit le résultat?

Résumé:

Je cherche le moyen le plus rapide de calculer

(int) x / (int) y

sans obtenir d'exception pour y==0. Au lieu de cela, je veux juste un résultat arbitraire.


Contexte:

Lors du codage d'algorithmes de traitement d'image, j'ai souvent besoin de diviser par une valeur alpha (cumulée). La variante la plus simple est un code C simple avec une arithmétique entière. Mon problème est que j'obtiens généralement une division par zéro d'erreur pour les pixels de résultat avec alpha==0. Cependant, ce sont exactement les pixels où le résultat n'a pas d'importance du tout: je ne me soucie pas des valeurs de couleur des pixels avec alpha==0.


Détails:

Je cherche quelque chose comme:

result = (y==0)? 0 : x/y;

ou

result = x / MAX( y, 1 );

x et y sont des entiers positifs. Le code est exécuté un grand nombre de fois dans une boucle imbriquée, donc je cherche un moyen de se débarrasser de la ramification conditionnelle.

Lorsque y ne dépasse pas la plage d'octets, je suis satisfait de la solution

unsigned char kill_zero_table[256] = { 1, 1, 2, 3, 4, 5, 6, 7, [...] 255 };
[...]
result = x / kill_zero_table[y];

Mais cela ne fonctionne évidemment pas bien pour des gammes plus importantes.

Je suppose que la dernière question est: Quel est le piratage de bits le plus rapide qui change 0 en toute autre valeur entière, tout en laissant toutes les autres valeurs inchangées?


Clarifications

Je ne suis pas sûr à 100% que le branchement soit trop cher. Cependant, différents compilateurs sont utilisés, donc je préfère le benchmarking avec peu d'optimisations (ce qui est en effet discutable).

Bien sûr, les compilateurs sont excellents en ce qui concerne le twiddling des bits, mais je ne peux pas exprimer le résultat "peu importe" en C, donc le compilateur ne pourra jamais utiliser la gamme complète d'optimisations.

Le code doit être entièrement compatible C, les principales plates-formes sont Linux 64 bits avec gcc & clang et MacOS.

109
philipp

Inspiré par certains commentaires, je me suis débarrassé de la branche sur mon Pentium et du compilateur gcc en utilisant

int f (int x, int y)
{
        y += y == 0;
        return x/y;
}

Le compilateur reconnaît essentiellement qu'il peut utiliser un indicateur de condition du test dans l'addition.

Conformément à la demande, l'Assemblée:

.globl f
    .type   f, @function
f:
    pushl   %ebp
    xorl    %eax, %eax
    movl    %esp, %ebp
    movl    12(%ebp), %edx
    testl   %edx, %edx
    sete    %al
    addl    %edx, %eax
    movl    8(%ebp), %edx
    movl    %eax, %ecx
    popl    %ebp
    movl    %edx, %eax
    sarl    $31, %edx
    idivl   %ecx
    ret

Comme cela s'est avéré être une question et une réponse si populaires, je vais élaborer un peu plus. L'exemple ci-dessus est basé sur un idiome de programmation reconnu par un compilateur. Dans le cas ci-dessus, une expression booléenne est utilisée en arithmétique intégrale et l'utilisation d'indicateurs de condition est inventée dans le matériel à cet effet. En général, les drapeaux d'état ne sont accessibles qu'en C via l'idiome. C'est pourquoi il est si difficile de créer une bibliothèque d'entiers à précision multiple portable en C sans recourir à l'assemblage (en ligne). Je suppose que la plupart des compilateurs décents comprendront l'idiome ci-dessus.

Une autre façon d'éviter les branches, comme le remarquent également certains des commentaires ci-dessus, est l'exécution prédictive. J'ai donc pris le premier code de philipp et mon code et l'ai exécuté à travers le compilateur à partir de ARM et le compilateur GCC pour l'architecture ARM, qui comporte une exécution prédictive. Les deux les compilateurs évitent la branche dans les deux exemples de code:

Version de Philipp avec le compilateur ARM:

f PROC
        CMP      r1,#0
        BNE      __aeabi_idivmod
        MOVEQ    r0,#0
        BX       lr

Version de Philipp avec GCC:

f:
        subs    r3, r1, #0
        str     lr, [sp, #-4]!
        moveq   r0, r3
        ldreq   pc, [sp], #4
        bl      __divsi3
        ldr     pc, [sp], #4

Mon code avec le compilateur ARM:

f PROC
        RSBS     r2,r1,#1
        MOVCC    r2,#0
        ADD      r1,r1,r2
        B        __aeabi_idivmod

Mon code avec GCC:

f:
        str     lr, [sp, #-4]!
        cmp     r1, #0
        addeq   r1, r1, #1
        bl      __divsi3
        ldr     pc, [sp], #4

Toutes les versions nécessitent toujours une branche pour la routine de division, car cette version de ARM n'a pas de matériel pour une division, mais le test pour y == 0 est entièrement implémenté par une exécution prédictive.

107
Bryan Olivier

Voici quelques chiffres concrets, sous Windows utilisant GCC 4.7.2:

#include <stdio.h>
#include <stdlib.h>

int main()
{
  unsigned int result = 0;
  for (int n = -500000000; n != 500000000; n++)
  {
    int d = -1;
    for (int i = 0; i != ITERATIONS; i++)
      d &= Rand();

#if CHECK == 0
    if (d == 0) result++;
#Elif CHECK == 1
    result += n / d;
#Elif CHECK == 2
    result += n / (d + !d);
#Elif CHECK == 3
    result += d == 0 ? 0 : n / d;
#Elif CHECK == 4
    result += d == 0 ? 1 : n / d;
#Elif CHECK == 5
    if (d != 0) result += n / d;
#endif
  }
  printf("%u\n", result);
}

Notez que je n'appelle pas intentionnellement srand(), de sorte que Rand() renvoie toujours exactement les mêmes résultats. Notez également que -DCHECK=0 Ne fait que compter les zéros, de sorte que la fréquence d'apparition est évidente.

Maintenant, compilez et chronométrez de différentes manières:

$ for it in 0 1 2 3 4 5; do for ch in 0 1 2 3 4 5; do gcc test.cc -o test -O -DITERATIONS=$it -DCHECK=$ch && { time=`time ./test`; echo "Iterations $it, check $ch: exit status $?, output $time"; }; done; done

affiche la sortie qui peut être résumée dans un tableau:

Iterations → | 0        | 1        | 2        | 3         | 4         | 5
-------------+-------------------------------------------------------------------
Zeroes       | 0        | 1        | 133173   | 1593376   | 135245875 | 373728555
Check 1      | 0m0.612s | -        | -        | -         | -         | -
Check 2      | 0m0.612s | 0m6.527s | 0m9.718s | 0m13.464s | 0m18.422s | 0m22.871s
Check 3      | 0m0.616s | 0m5.601s | 0m8.954s | 0m13.211s | 0m19.579s | 0m25.389s
Check 4      | 0m0.611s | 0m5.570s | 0m9.030s | 0m13.544s | 0m19.393s | 0m25.081s
Check 5      | 0m0.612s | 0m5.627s | 0m9.322s | 0m14.218s | 0m19.576s | 0m25.443s

Si les zéros sont rares, la version -DCHECK=2 Fonctionne mal. Au fur et à mesure que les zéros commencent à apparaître, le cas -DCHECK=2 Commence à fonctionner beaucoup mieux. Parmi les autres options, il n'y a vraiment pas beaucoup de différence.

Pour -O3, Cependant, c'est une autre histoire:

Iterations → | 0        | 1        | 2        | 3         | 4         | 5
-------------+-------------------------------------------------------------------
Zeroes       | 0        | 1        | 133173   | 1593376   | 135245875 | 373728555
Check 1      | 0m0.646s | -        | -        | -         | -         | -
Check 2      | 0m0.654s | 0m5.670s | 0m9.905s | 0m14.238s | 0m17.520s | 0m22.101s
Check 3      | 0m0.647s | 0m5.611s | 0m9.085s | 0m13.626s | 0m18.679s | 0m25.513s
Check 4      | 0m0.649s | 0m5.381s | 0m9.117s | 0m13.692s | 0m18.878s | 0m25.354s
Check 5      | 0m0.649s | 0m6.178s | 0m9.032s | 0m13.783s | 0m18.593s | 0m25.377s

Là, le chèque 2 n'a aucun inconvénient par rapport aux autres chèques, et il conserve les avantages à mesure que les zéros deviennent plus courants.

Vous devriez vraiment mesurer pour voir ce qui se passe avec votre compilateur et vos données d'exemple représentatives.

20
user743382

Sans connaître la plate-forme, il n'y a aucun moyen de connaître la méthode exacte la plus efficace, cependant, sur un système générique, cela peut être proche de l'optimum (en utilisant la syntaxe de l'assembleur Intel):

(supposons que le diviseur est dans ecx et que le dividende est dans eax)

mov ebx, ecx
neg ebx
sbb ebx, ebx
add ecx, ebx
div eax, ecx

Quatre instructions non ramifiées à cycle unique plus la division. Le quotient sera dans eax et le reste sera dans edx à la fin. (Ce type montre pourquoi vous ne voulez pas envoyer un compilateur pour faire le travail d'un homme).

13
Tyler Durden

Selon cela link , vous pouvez simplement bloquer le signal SIGFPE avec sigaction() (je ne l'ai pas essayé moi-même, mais je pense que cela devrait fonctionner).

C'est l'approche la plus rapide possible si les erreurs de division par zéro sont extrêmement rares: vous ne payez que pour les divisions par zéro, pas pour les divisions valides, le chemin d'exécution normal n'est pas changé du tout.

Cependant, le système d'exploitation sera impliqué dans chaque exception ignorée, ce qui est coûteux. Je pense que vous devriez avoir au moins mille bonnes divisions par division par zéro que vous ignorez. Si les exceptions sont plus fréquentes que cela, vous paierez probablement plus en ignorant les exceptions qu'en vérifiant chaque valeur avant la division.