Actualmente estoy tratando de mejorar el rendimiento de un programa F # para que sea tan rápido como su equivalente en C#. El programa aplica una matriz de filtros a un búfer de píxeles. El acceso a la memoria siempre se hace usando punteros.Problema de rendimiento de manipulación de imágenes F #
Este es el código C# que se aplica a cada píxel de una imagen:
unsafe private static byte getPixelValue(byte* buffer, double* filter, int filterLength, double filterSum)
{
double sum = 0.0;
for (int i = 0; i < filterLength; ++i)
{
sum += (*buffer) * (*filter);
++buffer;
++filter;
}
sum = sum/filterSum;
if (sum > 255) return 255;
if (sum < 0) return 0;
return (byte) sum;
}
El # código F se parece a esto y toma tres veces más largo que el programa de C#:
let getPixelValue (buffer:nativeptr<byte>) (filterData:nativeptr<float>) filterLength filterSum : byte =
let rec accumulatePixel (acc:float) (buffer:nativeptr<byte>) (filter:nativeptr<float>) i =
if i > 0 then
let newAcc = acc + (float (NativePtr.read buffer) * (NativePtr.read filter))
accumulatePixel newAcc (NativePtr.add buffer 1) (NativePtr.add filter 1) (i-1)
else
acc
let acc = (accumulatePixel 0.0 buffer filterData filterLength)/filterSum
match acc with
| _ when acc > 255.0 -> 255uy
| _ when acc < 0.0 -> 0uy
| _ -> byte acc
El uso de variables mutables y un bucle for en F # da como resultado la misma velocidad que el uso de la recursión. Todos los proyectos están configurados para ejecutarse en modo de lanzamiento con la optimización de código activada.
¿Cómo se podría mejorar el rendimiento de la versión F #?
EDIT:
El cuello de botella parece estar en (NativePtr.get buffer offset)
. Si reemplazo este código con un valor fijo y también reemplazo el código correspondiente en la versión C# con un valor fijo, obtengo la misma velocidad para ambos programas. De hecho, en C# la velocidad no cambia en absoluto, pero en F # hace una gran diferencia.
¿Se puede cambiar este comportamiento o se arraiga profundamente en la arquitectura de F #?
EDIT 2:
I rediseñado el código de nuevo utilizar para de bucles. La velocidad de ejecución sigue siendo el mismo:
let mutable acc <- 0.0
let mutable f <- filterData
let mutable b <- tBuffer
for i in 1 .. filter.FilterLength do
acc <- acc + (float (NativePtr.read b)) * (NativePtr.read f)
f <- NativePtr.add f 1
b <- NativePtr.add b 1
Si se compara el código IL de una versión que utiliza (NativePtr.read b)
y otra versión que es la misma excepto que utiliza un valor fijo 111uy
en lugar de leerlo desde el puntero, Sólo las siguientes líneas en el cambio de código IL:
111uy
tiene IL-Code ldc.i4.s 0x6f
(0,3 segundos)
(NativePtr.read b)
tiene líneas de IL-código ldloc.s b
y ldobj uint8
(1,4 segundos)
Para comparar: C# hace el filtrado en 0.4 segundos.
El hecho de que leer el filtro no afecte el rendimiento mientras se lee desde el búfer de imagen es de alguna manera confuso. Antes de filtrar una línea de la imagen, copio la línea en un buffer que tiene la longitud de una línea. Es por eso que las operaciones de lectura no se extienden por toda la imagen, sino que se encuentran dentro de este búfer, que tiene un tamaño de aproximadamente 800 bytes.
Basado en su comentario más reciente, me pregunto si el hecho de que el compilador de C# utiliza 'ldind.u1' en lugar de 'ldobj uint8' (que es el IL semánticamente equivalente que usa F #) hace la diferencia. Podría intentar ejecutar ildasm en el ejecutable F #, reemplazando 'ldobj uint8' con' ldind.u1', y ejecutando ilasm en él para ver si el rendimiento es diferente. – kvb
Lo reemplacé pero no hubo diferencia. Gracias de cualquier manera. –