F # проблема производительности манипуляции с изображениями - PullRequest
9 голосов
/ 19 января 2011

В настоящее время я пытаюсь улучшить производительность программы на 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 buffer offset).Если я заменю этот код с фиксированным значением, а также заменим соответствующий код в версии 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.s b и ldobj uint8 (1,4 секунды)

Для сравнения: C # выполняет фильтрацию за 0,4 секунды.

Тот факт, что чтение фильтра не влияет на производительность при чтении из буфера изображения, делаеткак-то сбивает с толку.Прежде чем отфильтровать строку изображения, я копирую строку в буфер, длина которого равна длине строки.Вот почему операции чтения распределяются не по всему изображению, а находятся внутри этого буфера, который имеет размер около 800 байтов.

Ответы [ 3 ]

12 голосов
/ 20 января 2011

Если мы посмотрим на фактический код 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 #))) 
1 голос
/ 21 января 2011

Мои результаты по большему тесту.

    adrian 6374730426.098020 1561.102500 ms
   gradbot 6374730426.098020 1842.768000 ms
        C# 6374730426.098020  150.793500 ms
 C# Offset 6374730426.098020  150.318900 ms
   mutable 6374730426.098020 1446.616700 ms

F # тестовый код

open Microsoft.FSharp.NativeInterop
open System.Runtime.InteropServices
open System.Diagnostics

open AccumulatePixel
#nowarn "9"

let test size fn =
    let bufferByte = Marshal.AllocHGlobal(size * 4)
    let bufferFloat = Marshal.AllocHGlobal(size * 8)

    let bi = NativePtr.ofNativeInt bufferByte
    let bf = NativePtr.ofNativeInt bufferFloat

    let random = System.Random()

    for i in 1 .. size do
        NativePtr.set bi i (byte <| random.Next() % 256)
        NativePtr.set bf i (random.NextDouble())

    let duration (f, name) =
        let stopWatch = Stopwatch.StartNew()
        let time = f(0.0, bi, bf, size)
        stopWatch.Stop()
        printfn "%10s %f %f ms" name time stopWatch.Elapsed.TotalMilliseconds

    List.iter duration fn

    Marshal.FreeHGlobal bufferFloat
    Marshal.FreeHGlobal bufferByte

let rec accumulatePixel_adrian (acc, buffer, filter, i) = 
    if i > 0 then
        let newAcc = acc + (float (NativePtr.read buffer) * (NativePtr.read filter))
        accumulatePixel_adrian (newAcc, (NativePtr.add buffer 1), (NativePtr.add filter 1), (i - 1))
    else
        acc

let accumulatePixel_gradbot (acc, buffer, filter, length) = 
    let rec accumulate acc offset = 
        if offset < length then
            let newAcc = acc + (float (NativePtr.get buffer offset) * (NativePtr.get filter offset))
            accumulate newAcc (offset + 1)
        else
            acc
    accumulate acc 0

let accumulatePixel_mutable (acc, buffer, filter, length) = 
    let mutable acc = 0.0
    let mutable f = filter
    let mutable b = buffer
    for i in 1 .. length do
        acc <- acc + (float (NativePtr.read b)) * (NativePtr.read f)
        f <- NativePtr.add f 1
        b <- NativePtr.add b 1
    acc

[
    accumulatePixel_adrian, "adrian";
    accumulatePixel_gradbot, "gradbot";
    AccumulatePixel.getPixelValue, "C#";
    AccumulatePixel.getPixelValueOffset, "C# Offset";
    accumulatePixel_mutable, "mutable";
]
|> test 100000000

System.Console.ReadLine() |> ignore

C # тестовый код

namespace AccumulatePixel
{
    public class AccumulatePixel
    {
        unsafe public static double getPixelValue(double sum, byte* buffer, double* filter, int filterLength)
        {
            for (int i = 0; i < filterLength; ++i)
            {
                sum += (*buffer) * (*filter);
                ++buffer;
                ++filter;
            }

            return sum;
        }

        unsafe public static double getPixelValueOffset(double sum, byte* buffer, double* filter, int filterLength)
        {
            for (int i = 0; i < filterLength; ++i)
            {
                sum += buffer[i] * filter[i];
            }

            return sum;
        }
    }
}
1 голос
/ 20 января 2011

Как это сравнить?У него меньше вызовов на NativePtr.

let getPixelValue (buffer:nativeptr<byte>) (filterData:nativeptr<float>) filterLength filterSum : byte =
    let accumulatePixel (acc:float) (buffer:nativeptr<byte>) (filter:nativeptr<float>) length = 
        let rec accumulate acc offset = 
            if offset < length then
                let newAcc = acc + (float (NativePtr.get buffer offset) * (NativePtr.get filter offset))
                accumulate newAcc (offset + 1)
            else
                acc
        accumulate acc 0

    let acc = (accumulatePixel 0.0 buffer filterData filterLength) / filterSum                

    match acc with
        | _ when acc > 255.0 -> 255uy
        | _ when acc < 0.0 -> 0uy
        | _ -> byte acc

F # исходный код NativePtr.

[<NoDynamicInvocation>]
[<CompiledName("AddPointerInlined")>]
let inline add (x : nativeptr<'T>) (n:int) : nativeptr<'T> = toNativeInt x + nativeint n * (# "sizeof !0" type('T) : nativeint #) |> ofNativeInt

[<NoDynamicInvocation>]
[<CompiledName("GetPointerInlined")>]
let inline get (p : nativeptr<'T>) n = (# "ldobj !0" type ('T) (add p n) : 'T #) 
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...