我目前正在尝试提高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
在F#中使用可变变量和for循环的结果与使用递归的速度相同。将所有项目配置为在“代码优化”处于打开状态的 Release模式下运行。
如何提高F#版本的性能?
编辑:
瓶颈似乎在
(NativePtr.get buffer offset)
中。如果我将此代码替换为固定值,并且还将C#版本中的相应代码替换为固定值,则两个程序的速度大致相同。实际上,在C#中,速度根本没有改变,但是在F#中,速度却有很大的不同。可以改变这种行为,还是可以将其深深 Root 于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
如果我比较使用
(NativePtr.read b)
的版本和另一个版本相同的IL代码,不同之处在于它使用固定值111uy
而不是从指针读取它,则IL代码中仅以下几行发生了变化:111uy
具有IL-code ldc.i4.s 0x6f
(0.3秒)(NativePtr.read b)
具有IL代码行ldloc.s b
和ldobj uint8
(1.4秒)为了进行比较:C#在0.4秒内进行了过滤。
从图像缓冲区读取数据时,读取过滤器不会影响性能,这一事实在某种程度上令人困惑。在过滤图像的一行之前,我先将该行复制到具有一行长度的缓冲区中。这就是为什么读取操作不会散布在整个图像上,而是在此缓冲区内的原因,该缓冲区的大小约为800字节。
最佳答案
如果我们看一下内部循环的实际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#中“ Root ”的,只是
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
。不确定为什么会这样,但是通过以下hack,我的F#版本在gradbot测试示例上以与C#相同的速度运行(顺便说一句,感谢 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 #)))