2010-11-04 13 views

risposta

2

Sto rivisitando questa domanda cinque anni dopo, poiché questo è rilevante anche per me. Ho eseguito alcune misurazioni delle prestazioni su due versioni pure C e due inline-assembly per x86-64, e i risultati potrebbero essere interessanti.

Le varianti testate della divisione pavimenti sono:

  1. L'implementazione ho usato per qualche tempo;
  2. La leggera variante su quella presentata sopra che utilizza solo una divisione;
  3. Il precedente, ma implementato manualmente in inline-assembly; e
  4. Una versione CMOV implementata in assembly.

Quello che segue è il mio programma di riferimento:

#include <stdio.h> 
#include <stdlib.h> 
#include <sys/time.h> 

#ifndef VARIANT 
#define VARIANT 3 
#endif 

#if VARIANT == 0 
#define floordiv(a, b) (((a) < 0)?((((a) + 1)/(b)) - 1):((a)/(b))) 
#elif VARIANT == 1 
#define floordiv(a, b) ((((a) < 0)?((a) - ((b) - 1)):(a))/(b)) 
#elif VARIANT == 2 
#define floordiv(a, b) ({         \ 
    int result;            \ 
    asm("test %%eax, %%eax; jns 1f; sub %1, %%eax;"   \ 
     "add $1, %%eax; 1: cltd; idivl %1;"     \ 
     : "=a" (result)          \ 
     : "r" (b),           \ 
      "0" (a)           \ 
     : "rdx");           \ 
    result;}) 
#elif VARIANT == 3 
#define floordiv(a, b) ({           \ 
    int result;              \ 
    asm("mov %%eax, %%edx; sub %1, %%edx; add $1, %%edx;"   \ 
     "test %%eax, %%eax; cmovs %%edx, %%eax; cltd;"    \ 
     "idivl %1;"             \ 
     : "=a" (result)            \ 
     : "r" (b),             \ 
      "0" (a)             \ 
     : "rdx");             \ 
    result;}) 
#endif 

double ntime(void) 
{ 
    struct timeval tv; 

    gettimeofday(&tv, NULL); 
    return(tv.tv_sec + (((double)tv.tv_usec)/1000000.0)); 
} 

void timediv(int n, int *p, int *q, int *r) 
{ 
    int i; 

    for(i = 0; i < n; i++) 
     r[i] = floordiv(p[i], q[i]); 
} 

int main(int argc, char **argv) 
{ 
    int n, i, *q, *p, *r; 
    double st; 

    n = 10000000; 
    p = malloc(sizeof(*p) * n); 
    q = malloc(sizeof(*q) * n); 
    r = malloc(sizeof(*r) * n); 
    for(i = 0; i < n; i++) { 
     p[i] = (rand() % 1000000) - 500000; 
     q[i] = (rand() % 1000000) + 1; 
    } 

    st = ntime(); 
    for(i = 0; i < 100; i++) 
     timediv(n, p, q, r); 
    printf("%g\n", ntime() - st); 
    return(0); 
} 

ho compilato questo con gcc -march=native -Ofast usando GCC 4.9.2, ed i risultati, il mio core i5-2400, sono stati i seguenti. I risultati sono abbastanza riproducibili da una corsa all'altra - atterrano sempre nello stesso ordine, almeno.

  • Variante 0: 7.21 secondi
  • Variante 1: 7,26 secondi
  • Variante 2: 6.73 secondi
  • Variante 3: 4.32 secondi

Così l'attuazione CMOV soffia gli altri fuori l'acqua, almeno. Ciò che mi sorprende è che la variante 2 out-fa la sua versione in puro C (variante 1) con un margine abbastanza ampio. Avrei pensato che il compilatore dovesse essere in grado di emettere codice almeno efficiente quanto il mio.

Qui ci sono alcune altre piattaforme, per il confronto:

AMD Athlon 64 X2 4200+, GCC 4.7.2:

  • Variante 0: 26.33 secondi
  • Variante 1: 25,38 secondi
  • Variante 2: 25.19 secondi
  • Variante 3: 22.39 secondi

Xeon E3-1271 v3, GCC 4.9.2:

  • Variante 0: 5.95 secondi
  • Variante 1: 5,62 secondi
  • Variante 2: 5.40 secondi
  • Variante 3: 3,44 secondi
+0

Si noti che Varianti 0 e 1 (non hanno controllato gli altri) forniscono solo i risultati numericamente corretti per la divisione del piano quando il divisore è positivo. (E in tal caso, i risultati coincidono anche con la divisione euclidea.) Se per esempio a, b = 5, -3, queste formule dicono che il quoziente è -1, ma il risultato per la divisione floor dovrebbe essere -2. (La divisione euclidea per 5, -3 è -1, ma queste formule danno una risposta diversa rispetto alla divisione sia floor che euclidea quando entrambi gli operandi sono negativi.) – dubiousjim

+0

Hai versioni benchmark in cui il divisore è una costante positiva (ma il dividendo è calcolato in fase di esecuzione)? Per il potere di due dividendi, l'operatore di turno a destra sarebbe quasi certamente il più veloce di qualsiasi altra alternativa, e probabilmente più leggibile di qualsiasi cosa che sarebbe altrettanto efficiente. Per altri dividendi, il codice ottimale per la divisione floored/Euclidian dovrebbe essere più veloce del codice per troncato, ma non conosco alcun modo affidabile per convincere i compilatori a generare un buon codice. – supercat

+0

@supercat: per i divisori costanti in fase di compilazione, il compilatore sarà in grado di generare codice migliore indipendentemente dal fatto che sia un power-of-two o meno, utilizzando [questo trucco] (https://stackoverflow.com/questions/30790184/perform -integer-divisione-using-moltiplicazione). Quindi sì, le divisioni con i divisori costanti in fase di compilazione saranno sicuramente più veloci di qualsiasi algoritmo che gestisca i divisori dinamici. – Dolda2000

0

Dal IEEE-754 specifica rotonda verso -inf come una delle modalità di arrotondamento richieste immagino che la risposta alla tua domanda è sì. Ma forse puoi spiegare se vuoi sapere come si implementerebbe la procedura se uno stava scrivendo il compilatore, o per sapere come usare un particolare compilatore per eseguire l'operazione?

+0

Whoops, intendevo divisione intera! Correggerò la domanda Non sono sicuro di cosa abbia a che fare la scrittura dei compilatori con la domanda, poiché ho controllato e C/C++ ha suddiviso la divisione. –

+0

Si sta facendo contorto –

+0

@CyberShadow: quindi lancia il divisore e il dividendo a numeri in virgola mobile di qualche tipo, usa floor e restituisci il risultato a un numero intero. O stai cercando qualche altro tipo di risposta? –

1

È possibile implementare in modo efficiente la divisione di interi o floored euclidian in C/C++?

Sì.

(la soluzione più ovvia è quello di verificare il segno del dividendo)

Sono completamente d'accordo, e avrebbero trovato difficile credere esiste un'alternativa che è significativamente più veloce.

2

Potrebbe essere più efficiente trovare un ramo libero per correggere il risultato in base al segno, poiché i rami sono costosi.

Vedere pagina 20ff di Chapter 2 in Hacker's Delight su come accedere al segno.

+0

L'unica cosa che posso pensare riguarda la moltiplicazione del bit di segno con il divisore. –

+0

È possibile rappresentare determinate condizioni per 0 o 1 e aggiungerle al risultato. – starblue

8

Ho scritto un programma di test al benchmark le idee presentate qui:

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

#define N 10000000 
#define M 100 

int dividends[N], divisors[N], results[N]; 

__forceinline int floordiv_signcheck(int a, int b) 
{ 
    return (a<0 ? a-(b-1) : a)/b; 
} 

__forceinline int floordiv_signcheck2(int a, int b) 
{ 
    return (a - (a<0 ? b-1 : 0))/b; 
} 

__forceinline int floordiv_signmultiply(int a, int b) 
{ 
    return (a + (a>>(sizeof(a)*8-1))*(b-1))/b; 
} 

__forceinline int floordiv_floatingpoint(int a, int b) 
{ 
    // I imagine that the call to floor can be replaced to a cast 
    // if you can get FPU rounding control to work (I couldn't). 
    return floor((double)a/b); 
} 

void main() 
{ 
    for (int i=0; i<N; i++) 
    { 
     dividends[i] = rand(); 
     do 
      divisors[i] = rand(); 
     while (divisors[i]==0); 
    } 

    LARGE_INTEGER t0, t1; 

    QueryPerformanceCounter(&t0); 
    for (int j=0; j<M; j++) 
     for (int i=0; i<N; i++) 
      results[i] = floordiv_signcheck(dividends[i], divisors[i]); 
    QueryPerformanceCounter(&t1); 
    printf("signcheck : %9llu\n", t1.QuadPart-t0.QuadPart); 

    QueryPerformanceCounter(&t0); 
    for (int j=0; j<M; j++) 
     for (int i=0; i<N; i++) 
      results[i] = floordiv_signcheck2(dividends[i], divisors[i]); 
    QueryPerformanceCounter(&t1); 
    printf("signcheck2 : %9llu\n", t1.QuadPart-t0.QuadPart); 

    QueryPerformanceCounter(&t0); 
    for (int j=0; j<M; j++) 
     for (int i=0; i<N; i++) 
      results[i] = floordiv_signmultiply(dividends[i], divisors[i]); 
    QueryPerformanceCounter(&t1); 
    printf("signmultiply : %9llu\n", t1.QuadPart-t0.QuadPart); 

    QueryPerformanceCounter(&t0); 
    for (int j=0; j<M; j++) 
     for (int i=0; i<N; i++) 
      results[i] = floordiv_floatingpoint(dividends[i], divisors[i]); 
    QueryPerformanceCounter(&t1); 
    printf("floatingpoint: %9llu\n", t1.QuadPart-t0.QuadPart); 
} 

Risultati:

signcheck : 61458768 
signcheck2 : 61284370 
signmultiply : 61625076 
floatingpoint: 287315364 

Così, secondo i miei risultati, controllando il segno è il più veloce:

(a - (a<0 ? b-1 : 0))/b 
+0

I tempi per tutti tranne il primo includono 'printf' per la risposta precedente. 'printf' è piuttosto lento, non so se è abbastanza lento da influenzare i risultati o meno. –

+0

Potresti anche essere influenzato dall'integrare le decisioni prese dal compilatore, varrebbe la pena esaminare l'assembly generato. Prova 'double' invece di' float' perché potrebbero esserci conversioni che si verificano anche lì. –

+0

Grazie per i suggerimenti, ho aggiornato il programma. L'uso di 'double' ha causato che il punto di virgola mobile funzioni un po 'più velocemente, ma non di molto. Altrimenti, il risultato non è cambiato molto. –

1

Solo una nota: l'istruzione x86 sar esegue la divisione floored quando si tratta di poteri di due.