Adr*_* H. 9 performance f# image-processing
我目前正在尝试提高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;
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
在F#中使用可变变量和for循环确实会产生与使用递归相同的速度.所有项目都配置为在发布模式下运行,并启用代码优化.
如何改进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
Run Code Online (Sandbox Code Playgroud)
如果我比较使用的版本的IL代码(NativePtr.read b)
和使用固定值111uy
而不是从指针读取它的另一个版本相同的另一个版本,只有IL代码中的以下行更改:
111uy
有IL-Code ldc.i4.s 0x6f
(0.3秒)
(NativePtr.read b)
有IL-Code线 ldloc.s b
和ldobj uint8
(1.4秒)
为了比较:C#在0.4秒内完成过滤.
从图像缓冲区读取时读取过滤器不影响性能的事实确实令人困惑.在我过滤图像的一行之前,我将该行复制到一个长度为一行的缓冲区中.这就是为什么读操作不是遍布整个图像而是在这个缓冲区内的原因,该缓冲区的大小约为800字节.
Ed'*_*'ka 13
如果我们查看内部循环的实际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
Run Code Online (Sandbox Code Playgroud)
和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
Run Code Online (Sandbox Code Playgroud)
我们会注意到,虽然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
Run Code Online (Sandbox Code Playgroud)
所以它并没有"深入"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
Run Code Online (Sandbox Code Playgroud)
另一个想法:它可能还取决于你的测试对getPixelValue的调用次数(F#将此函数拆分为两种方法,而C#将其合并为一种).
您是否有可能在此处发布测试代码?
关于数组 - 我希望代码至少更简洁(而不是unsafe
).
更新#2:
看起来这里的实际瓶颈是byte->float
转换.
C#:
L_0003: ldarg.1
L_0004: ldind.u1
L_0005: conv.r8
Run Code Online (Sandbox Code Playgroud)
F#:
L_000c: ldarg.1
L_000d: ldobj uint8
L_0012: conv.r.un
L_0013: conv.r8
Run Code Online (Sandbox Code Playgroud)
出于某种原因,F#使用以下路径:byte->float32->float64
而C#仅执行此操作byte->float64
.不知道为什么会这样,但是在下面的黑客中,我的F#版本在gradbot测试样本上以与C#相同的速度运行(BTW,感谢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
Run Code Online (Sandbox Code Playgroud)
结果:
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
Run Code Online (Sandbox Code Playgroud)
最后更新 事实证明,即使没有任何黑客,我们也可以达到相同的速度:
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
Run Code Online (Sandbox Code Playgroud)
我们所需要做的就是转换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 #)))
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
1591 次 |
最近记录: |