2012-05-06 10 views
6

Me encontré con una optimización de código inesperada recientemente, y quería comprobar si mi interpretación de lo que estaba observando era correcta. El siguiente es un ejemplo muy simplificado de la situación:F # generalización y rendimiento automáticos

let demo = 
    let swap fst snd i = 
     if i = fst then snd else 
     if i = snd then fst else 
     i 
    [ for i in 1 .. 10000 -> swap 1 i i ] 

let demo2 = 
    let swap (fst: int) snd i = 
     if i = fst then snd else 
     if i = snd then fst else 
     i 
    [ for i in 1 .. 10000 -> swap 1 i i ] 

La única diferencia entre los 2 bloques de código es que en el segundo caso, declaro explícitamente los argumentos de intercambio como enteros. Sin embargo, cuando ejecuto los 2 fragmentos en fsi con #time, obtengo:

Caso 1 Real: 00: 00: 00.011, CPU: 00: 00: 00,000, GC gen0: 0, gen1: 0, gen2: 0
real Caso 2: 00: 00: 00,004, CPU: 00: 00: 00,015, GC Gen0: 0, Gen1: 0, Gen2: 0

es decir, el segundo fragmento se ejecuta 3 veces más rápido que el primero. La diferencia absoluta de rendimiento aquí obviamente no es un problema, pero si tuviera que usar la función de intercambio mucho, se acumularía.

Mi suposición es que el motivo del rendimiento alcanzado es que, en el primer caso, swap es genérico y "requiere igualdad", y verifica si int lo admite, mientras que el segundo caso no tiene que verificar nada. ¿Es esta la razón por la que esto está sucediendo o me estoy perdiendo algo más? Y, en términos más generales, ¿debería considerar la generalización automática como una espada de doble filo, es decir, una característica increíble que puede tener efectos inesperados en el rendimiento?

Respuesta

10

Creo que este es generalmente el mismo caso que en la pregunta Why is this F# code so slow. En esa pregunta, el problema de rendimiento es causado por una restricción que requiere comparison y en su caso, es causada por la restricción equality.

En ambos casos, el código genérico compilado tiene que usar interfaces (y boxeo), mientras que el código compilado especializado puede usar directamente las instrucciones IL para comparar o la igualdad de enteros o números de coma flotante.

Las dos formas de evitar el problema de rendimiento son:

  • especializan el código para utilizar int o float como lo hizo
  • Marque la función que inline para que el compilador se especializa automáticamente

Para funciones más pequeñas, el segundo enfoque es mejor, porque no genera código demasiado y aún puede escribir funciones de una manera genérica. Si usa la función solo para el tipo único (por diseño), probablemente sea apropiado usar el primer enfoque.

+0

Gracias por el puntero a la otra pregunta, de hecho muy similar. Entonces, si lo entiendo correctamente, al marcar una función como en línea dice "mientras esta función es genérica, crea una versión específica basada en los tipos utilizados donde se llama". ¿Hay alguna razón para no marcar cada función de estilo matemático como en línea? – Mathias

+0

O, dicho de otra manera, ¿puedes explicar un poco sobre "no genera demasiado código", que no entiendo del todo? – Mathias

+1

@Mathias La palabra clave 'inline' significa que el compilador reemplazará la llamada a la función con su implementación. Esto significa que el código de la función se repetirá una vez por cada llamada. Para funciones realmente largas, esto podría hacer que el ensamblaje sea más grande (o generar métodos más largos que requieran mucho tiempo para JIT). Es por eso que recomendé usar esto solo para funciones más cortas: las funciones en su ejemplo me parecen muy cortas. –

3

La razón de esta diferencia es que el compilador genera llamadas a

IL_0002: call bool class [FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives/HashCompare::GenericEqualityIntrinsic<!!0> (!!0, !!0) 

en la versión genérica, mientras que la versión int solo se puede comparar directamente.

Si utilizó inline sospecho que este problema desaparecería como el compilador ahora tiene información de tipo adicional

Cuestiones relacionadas