Attualmente sto cercando di migliorare le prestazioni di un programma F # per renderlo veloce quanto il suo equivalente C#. Il programma applica un array di filtri a un buffer di pixel. L'accesso alla memoria avviene sempre tramite puntatori.F # problema di prestazioni di manipolazione dell'immagine
Ecco il codice C# che viene applicato a ogni pixel di un'immagine:
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;
}
Il codice F # simile a questo e prende tre volte fino a quando il programma di 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
L'uso di variabili mutabili e un ciclo for in F # ha come risultato la ricorsione. Tutti i progetti sono configurati per l'esecuzione in modalità di rilascio con l'ottimizzazione del codice attivata.
Come è possibile migliorare le prestazioni della versione F #?
EDIT:
Il collo di bottiglia sembra essere in (NativePtr.get buffer offset)
. Se sostituisco questo codice con un valore fisso e sostituisco anche il codice corrispondente nella versione C# con un valore fisso, ottengo la stessa velocità per entrambi i programmi. Infatti, in C# la velocità non cambia affatto, ma in F # fa un'enorme differenza.
Questo comportamento può essere eventualmente modificato o radicato profondamente nell'architettura di F #?
EDIT 2:
ho riscritta nuovamente il codice da utilizzare per-loop. La velocità di esecuzione rimane la stessa:
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
Se confronto il codice IL di una versione che utilizza (NativePtr.read b)
e un'altra versione che è lo stesso eccetto che utilizza un valore fisso 111uy
invece di leggere dal puntatore, Solo le seguenti righe nel codice iL cambiamento:
111uy
comprende iL-codice ldc.i4.s 0x6f
(0,3 secondi)
(NativePtr.read b)
ha linee di iL-code ldloc.s b
e ldobj uint8
(1,4 secondi)
Per confronto: C# esegue il filtraggio in 0,4 secondi.
Il fatto che la lettura del filtro non influenzi le prestazioni durante la lettura dal buffer di immagini è in qualche modo fonte di confusione. Prima di filtrare una linea dell'immagine, copio la linea in un buffer che ha la lunghezza di una linea. Ecco perché le operazioni di lettura non sono distribuite su tutta l'immagine ma sono all'interno di questo buffer, che ha una dimensione di circa 800 byte.
In base al più recente commento, mi chiedo se il fatto che il compilatore C# utilizza 'ldind.u1' invece di 'ldobj uint8' (che è l'IL semanticamente equivalente usato da F #) fa la differenza. Si può provare a eseguire ildasm sull'eseguibile F #, sostituendo 'ldobj uint8' con' ldind.u1', e eseguendo ilmisura su di esso per vedere se le prestazioni differiscono. – kvb
L'ho sostituito ma non c'era differenza. Grazie comunque. –