В настоящее время я пытаюсь улучшить производительность программы на F #, чтобы сделать ее такой же быстрой, как ее эквивалент на C #. Программа действительно применяет массив фильтров к буферу пикселей. Доступ к памяти всегда осуществляется с помощью указателей.
Вот код C #, который применяется к каждому пикселю изображения:
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;
}
Код F # выглядит так и занимает в три раза больше времени, чем программа 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
Использование изменяемых переменных и цикла for в F # приводит к той же скорости, что и использование рекурсии. Все проекты настроены для работы в режиме выпуска с включенной оптимизацией кода.
Как можно улучшить производительность версии F #?
РЕДАКТИРОВАТЬ:
Узкое место, по-видимому, находится в (NativePtr.get смещение буфера)
. Если я заменю этот код фиксированным значением, а также заменю соответствующий код в версии C # фиксированным значением, я получу примерно одинаковую скорость для обеих программ. На самом деле, в C # скорость вообще не меняется, но в F # она имеет огромное значение.
Можно ли изменить это поведение или оно глубоко укоренилось в архитектуре F #?
РЕДАКТИРОВАТЬ 2:
Я снова реорганизовал код для использования циклов for. Скорость выполнения остается прежней:
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
Если я сравниваю код IL версии, которая использует (NativePtr.read b)
, и другой версии, которая такая же, за исключением того, что в ней используется фиксированное значение 111uy
вместо чтения из указателя. В коде IL меняются только следующие строки:
111uy
имеет код IL ldc.i4.s 0x6f
(0,3 секунды)
(NativePtr.read b)
имеет строки кода IL ldloc.sb
и ldobj uint8
(1,4 секунды)
Для сравнения: C # выполняет фильтрацию за 0,4 секунд.
Тот факт, что чтение фильтра не влияет на производительность, а чтение из буфера изображения, в некоторой степени сбивает с толку. Прежде чем фильтровать строку изображения, я копирую строку в буфер, имеющий длину строки. Вот почему операции чтения не распределяются по всему изображению, а находятся в этом буфере, размер которого составляет около 800 байт.