Если мы посмотрим на фактический код IL внутреннего цикла, который проходит через оба буфера параллельно, сгенерированный компилятором C # (соответствующая часть):
L_0017: ldarg.0
L_0018: ldc.i4.1
L_0019: conv.i
L_001a: add
L_001b: starg.s buffer
L_001d: ldarg.1
L_001e: ldc.i4.8
L_001f: conv.i
L_0020: add
и компилятор F #:
L_0017: ldc.i4.1
L_0018: conv.i
L_0019: sizeof uint8
L_001f: mul
L_0020: add
L_0021: ldarg.2
L_0022: ldc.i4.1
L_0023: conv.i
L_0024: sizeof float64
L_002a: mul
L_002b: add
мы заметим, что в то время как в коде C # используется только оператор add
, в то время как для F # требуются mul
и add
Но очевидно, что на каждом шаге нам нужно только увеличивать указатели (на значения 'sizeof byte' и 'sizeof float' соответственно), а не вычислять адрес (addrBase + (sizeof byte)) F # mul
не требуется (он всегда умножается на 1 ).
Причина этого в том, что C # определяет оператор ++
для указателей, а F # предоставляет только add : nativeptr<'T> -> int -> nativeptr<'T>
оператор:
[<NoDynamicInvocation>]
let inline add (x : nativeptr<'a>) (n:int) : nativeptr<'a> = to_nativeint x + nativeint n * (# "sizeof !0" type('a) : nativeint #) |> of_nativeint
Так что это не "глубоко укоренено" в F #, просто в module NativePtr
отсутствуют функции inc
и dec
.
Кстати, я подозреваю, что приведенный выше пример мог бы быть написан более кратко, если бы аргументы передавались как массивы, а не как необработанные указатели.
ОБНОВЛЕНИЕ:
Таким образом, следующий код имеет ускорение всего на 1% (похоже, что он очень похож на C # IL):
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.ofNativeInt <| (NativePtr.toNativeInt buffer) + (nativeint 1)) (NativePtr.ofNativeInt <| (NativePtr.toNativeInt filter) + (nativeint 8)) (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
Еще одна мысль: это может также зависеть от количества вызовов getPixelValue, которые выполняет ваш тест (F # разбивает эту функцию на два метода, а C # делает это в одном).
Возможно ли, что вы разместите здесь свой код тестирования?
Относительно массива - я ожидаю, что код будет как минимум более кратким (а не unsafe
).
ОБНОВЛЕНИЕ № 2:
Похоже, что фактическим узким местом здесь является byte->float
преобразование.
C #:
L_0003: ldarg.1
L_0004: ldind.u1
L_0005: conv.r8
F #:
L_000c: ldarg.1
L_000d: ldobj uint8
L_0012: conv.r.un
L_0013: conv.r8
По какой-то причине F # использует следующий путь: byte->float32->float64
, в то время как C # делает только byte->float64
. Не уверен, почему это так, но со следующим хаком моя версия F # работает с той же скоростью, что и C # на тестовом образце gradbot (кстати, спасибо gradbot за тест!):
let inline preadConvert (p : nativeptr<byte>) = (# "conv.r8" (# "ldobj !0" type (byte) p : byte #) : float #)
let inline pinc (x : nativeptr<'a>) : nativeptr<'a> = NativePtr.toNativeInt x + (# "sizeof !0" type('a) : nativeint #) |> NativePtr.ofNativeInt
let rec accumulatePixel_ed (acc, buffer, filter, i) =
if i > 0 then
accumulatePixel_ed
(acc + (preadConvert buffer) * (NativePtr.read filter),
(pinc buffer),
(pinc filter),
(i-1))
else
acc
Результаты:
adrian 6374985677.162810 1408.870900 ms
gradbot 6374985677.162810 1218.908200 ms
C# 6374985677.162810 227.832800 ms
C# Offset 6374985677.162810 224.921000 ms
mutable 6374985677.162810 1254.337300 ms
ed'ka 6374985677.162810 227.543100 ms
ПОСЛЕДНИЕ ОБНОВЛЕНИЯ
Оказалось, что мы можем достичь той же скорости даже без всяких взломов:
let rec accumulatePixel_ed_last (acc, buffer, filter, i) =
if i > 0 then
accumulatePixel_ed_last
(acc + (float << int16 <| NativePtr.read buffer) * (NativePtr.read filter),
(NativePtr.add buffer 1),
(NativePtr.add filter 1),
(i-1))
else
acc
Все, что нам нужно сделать, - это преобразовать byte
в, скажем, int16
, а затем в float
. Таким образом, «дорогостоящие» инструкции conv.r.un
будут исключены.
PS Соответствующий код преобразования из "prim-types.fs":
let inline float (x: ^a) =
(^a : (static member ToDouble : ^a -> float) (x))
when ^a : float = (# "" x : float #)
when ^a : float32 = (# "conv.r8" x : float #)
// [skipped]
when ^a : int16 = (# "conv.r8" x : float #)
// [skipped]
when ^a : byte = (# "conv.r.un conv.r8" x : float #)
when ^a : decimal = (System.Convert.ToDouble((# "" x : decimal #)))