2010-05-20 12 views
15

Actualmente estoy escribiendo algunos GLSL como las clases de vectores de matemáticas en C++, y acabo implementado una función abs() así:Qué es diferente en C++ abs math.h() en comparación con mi abs()

template<class T> 
static inline T abs(T _a) 
{ 
    return _a < 0 ? -_a : _a; 
} 

comparé su velocidad en el valor predeterminado C++ abs de math.h así:

clock_t begin = clock(); 
for(int i=0; i<10000000; ++i) 
{ 
    float a = abs(-1.25); 
}; 

clock_t end = clock(); 
unsigned long time1 = (unsigned long)((float)(end-begin)/((float)CLOCKS_PER_SEC/1000.0)); 

begin = clock(); 
for(int i=0; i<10000000; ++i) 
{ 
    float a = myMath::abs(-1.25); 
}; 
end = clock(); 
unsigned long time2 = (unsigned long)((float)(end-begin)/((float)CLOCKS_PER_SEC/1000.0)); 

std::cout<<time1<<std::endl; 
std::cout<<time2<<std::endl; 

Ahora los abdominales por defecto toma alrededor de 25 ms mientras que la mía tiene 60. Creo que hay algo de optimización bajo nivel pasando. ¿Alguien sabe cómo funciona math.habs internamente? La diferencia de rendimiento no es nada dramática, ¡pero solo tengo curiosidad!

+3

Quizás podría mostrar la fuente de ensamblaje generada por el compilador para los dos bucles y comparar eso. De lo contrario, solo estamos adivinando. –

+2

'abs' toma un número entero y devuelve un número entero. Entonces, usar 'abs' es simplemente incorrecto en tu comparación. Use 'fabs' de' 'en su lugar. – kennytm

+0

Y díganos cuál es su compilador/entorno. – jpalecek

Respuesta

1

Probablemente la versión de biblioteca de abs es una función intrínseca, cuyo comportamiento es exactamente conocido por el compilador, que incluso puede calcular el valor en tiempo de compilación (como en su caso se conoce) y optimizar la llamada. Debería probar su punto de referencia con un valor conocido solo en tiempo de ejecución (proporcionado por el usuario o obtenido con rand() antes de los dos ciclos).

Si todavía hay una diferencia, puede ser porque la biblioteca ABS está escrita directamente en ensamblaje forjado a mano con trucos de magia, por lo que podría ser un poco más rápido que el generado.

4

Es probable que sólo utiliza una máscara de bits para establecer el bit de signo a 0.

+0

hey, no tengo mucha experiencia en programación bit a bit, ¿cómo funcionaría eso? – moka

+0

@moka: ver http://en.wikipedia.org/wiki/Double_precision_floating-point_format. –

15

Puesto que son la puesta en práctica, son libres de hacer tantas suposiciones como ellos quieren. Conocen el formato del double y pueden jugar trucos con eso.

Probablemente (como en casi ninguna pregunta), su double es el binary64 format. Esto significa que el signo tiene su propio bit, y un valor absoluto es simplemente borrar ese bit. Por ejemplo, como una especialización, un implementador compilador puede hacer lo siguiente:

template <> 
double abs<double>(const double x) 
{ 
    // breaks strict aliasing, but compiler writer knows this behavior for the platform 
    uint64_t i = reinterpret_cast<const std::uint64_t&>(x); 
    i &= 0x7FFFFFFFFFFFFFFFULL; // clear sign bit 

    return reinterpret_cast<const double&>(i); 
} 

Esto elimina la ramificación y puede correr más rápido.

+0

¿No sería eso un claro ejemplo de comportamiento indefinido? – jpalecek

+5

@jpalecek: No veo ningún UB allí. 'reinterpret_cast' tiene un comportamiento definido por la implementación. (De ahí mi idea de que "pueden hacer las suposiciones específicas de la implementación que quieran, ya que son la implementación"). Los codificadores que no pertenecen a la biblioteca no pueden hacerlo. Una vez que escribe este código, está asumiendo ciertas cosas sobre su implementación. – GManNickG

+1

Si el doble no es IEEE-754, sí. Es al menos menos portátil –

3

Puede haber varias cosas:

  • ¿está seguro de la primera llamada utiliza std::abs? Es igual de bien podría utilizar el número entero abs de C (o bien llamar std::abs explícitamente, o tiene using std::abs;)

  • el compilador podría tener aplicación intrínseca de algunas funciones de flotador (por ejemplo. Compilar directamente en instrucciones FPU)

Sin embargo, me sorprende que el compilador no elimine por completo el bucle, ya que no se hace nada con ningún efecto dentro del bucle, y al menos en el caso de abs, el compilador debe saber que no hay side- efectos.

8

Existen trucos bien conocidos para calcular el valor absoluto del número firmado de un complemento de dos. Si el número es negativo, voltea todos los bits y agrega 1, es decir, xor con -1 y resta -1. Si es positivo, no haga nada, es decir, xor con 0 y reste 0.

int my_abs(int x) 
{ 
    int s = x >> 31; 
    return (x^s) - s; 
} 
+0

los valores de coma flotante no se almacenan en dos complementos, pero este es un truco ingenioso para el valor absoluto. (Para determinar el signo, hago el mismo primer paso pero luego devuelvo 's | 1'; -1 = negativo, +1 = positivo) –

+0

Esto es bueno, aunque fuera de tema aquí ... – jpalecek

+0

@jpal Bastante, pero ¿Dónde dice exactamente moka que solo le interesan los tipos de punto flotante? Tal vez la 'plantilla ' debería reemplazarse con 'plantilla ' entonces, solo para ser claros en esto? – fredoverflow

1

Su versión de ABS se colocarán en línea y se puede calcular de una vez el compilador puede trivialmente saber que el valor devuelto no va a cambiar, por lo que no necesita ni siquiera llamar a la función.

Realmente necesita ver el código ensamblador generado (establecer un punto de interrupción y abrir la vista del depurador "grande", este desensamblaje estará en la parte inferior izquierda si la memoria funciona), y luego puede ver lo que está sucediendo.

Puede encontrar la documentación de su procesador en línea sin demasiados problemas, le dirá cuáles son todas las instrucciones para que pueda descubrir qué está pasando. Alternativamente, pégalo aquí y te diremos. ;)

8

¿Cuál es su compilador y configuración? Estoy seguro de que MS y GCC implementan "funciones intrínsecas" para muchas operaciones de matemáticas y cadenas.

la línea siguiente:

printf("%.3f", abs(1.25)); 

cae en la siguiente ruta de código "FABS" (en msvcr90d.dll):

004113DE sub   esp,8 
004113E1 fld   qword ptr [[email protected] (415748h)] 
004113E7 fstp  qword ptr [esp] 
004113EA call  abs (4110FFh) 

abs llaman aplicación los C de tiempo de ejecución 'FABS' en MSVCR90D (bastante grande):

102F5730 mov   edi,edi 
102F5732 push  ebp 
102F5733 mov   ebp,esp 
102F5735 sub   esp,14h 
102F5738 fldz    
102F573A fstp  qword ptr [result] 
102F573D push  0FFFFh 
102F5742 push  133Fh 
102F5747 call  _ctrlfp (102F6140h) 
102F574C add   esp,8 
102F574F mov   dword ptr [savedcw],eax 
102F5752 movzx  eax,word ptr [ebp+0Eh] 
102F5756 and   eax,7FF0h 
102F575B cmp   eax,7FF0h 
102F5760 jne   fabs+0D2h (102F5802h) 
102F5766 sub   esp,8 
102F5769 fld   qword ptr [x] 
102F576C fstp  qword ptr [esp] 
102F576F call  _sptype (102F9710h) 
102F5774 add   esp,8 
102F5777 mov   dword ptr [ebp-14h],eax 
102F577A cmp   dword ptr [ebp-14h],1 
102F577E je   fabs+5Eh (102F578Eh) 
102F5780 cmp   dword ptr [ebp-14h],2 
102F5784 je   fabs+77h (102F57A7h) 
102F5786 cmp   dword ptr [ebp-14h],3 
102F578A je   fabs+8Fh (102F57BFh) 
102F578C jmp   fabs+0A8h (102F57D8h) 
102F578E push  0FFFFh 
102F5793 mov   ecx,dword ptr [savedcw] 
102F5796 push  ecx 
102F5797 call  _ctrlfp (102F6140h) 
102F579C add   esp,8 
102F579F fld   qword ptr [x] 
102F57A2 jmp   fabs+0F8h (102F5828h) 
102F57A7 push  0FFFFh 
102F57AC mov   edx,dword ptr [savedcw] 
102F57AF push  edx 
102F57B0 call  _ctrlfp (102F6140h) 
102F57B5 add   esp,8 
102F57B8 fld   qword ptr [x] 
102F57BB fchs    
102F57BD jmp   fabs+0F8h (102F5828h) 
102F57BF mov   eax,dword ptr [savedcw] 
102F57C2 push  eax 
102F57C3 sub   esp,8 
102F57C6 fld   qword ptr [x] 
102F57C9 fstp  qword ptr [esp] 
102F57CC push  15h 
102F57CE call  _handle_qnan1 (102F98C0h) 
102F57D3 add   esp,10h 
102F57D6 jmp   fabs+0F8h (102F5828h) 
102F57D8 mov   ecx,dword ptr [savedcw] 
102F57DB push  ecx 
102F57DC fld   qword ptr [x] 
102F57DF fadd  qword ptr [[email protected] (1022CF68h)] 
102F57E5 sub   esp,8 
102F57E8 fstp  qword ptr [esp] 
102F57EB sub   esp,8 
102F57EE fld   qword ptr [x] 
102F57F1 fstp  qword ptr [esp] 
102F57F4 push  15h 
102F57F6 push  8  
102F57F8 call  _except1 (102F99B0h) 
102F57FD add   esp,1Ch 
102F5800 jmp   fabs+0F8h (102F5828h) 
102F5802 mov   edx,dword ptr [ebp+0Ch] 
102F5805 and   edx,7FFFFFFFh 
102F580B mov   dword ptr [ebp-0Ch],edx 
102F580E mov   eax,dword ptr [x] 
102F5811 mov   dword ptr [result],eax 
102F5814 push  0FFFFh 
102F5819 mov   ecx,dword ptr [savedcw] 
102F581C push  ecx 
102F581D call  _ctrlfp (102F6140h) 
102F5822 add   esp,8 
102F5825 fld   qword ptr [result] 
102F5828 mov   esp,ebp 
102F582A pop   ebp 
102F582B ret 

En modo de liberación, se usa la instrucción FPU FABS en su lugar (toma 1 ciclo de reloj o ólo en FPU> = Pentium), la salida es Desensamblaje: función ABS

00401006 fld   qword ptr [[email protected] (402100h)] 
0040100C sub   esp,8 
0040100F fabs    
00401011 fstp  qword ptr [esp] 
00401014 push  offset string "%.3f" (4020F4h) 
00401019 call  dword ptr [__imp__printf (4020A0h)] 
1

La biblioteca opera en números enteros mientras se está probando, obviamente, flotadores. Esto significa que llamar a abs con argumento flotante implica la conversión de float a int (puede ser un no-op ya que está usando constant y el compilador puede hacerlo en tiempo de compilación), luego INTEGER abs operation y conversion int-> float. La función de plantilla involucrará operaciones en flotadores y esto probablemente está haciendo la diferencia.

Cuestiones relacionadas