我整天在对一个应用程序进行性能分析,优化了一些代码,但还剩下这个任务。这是神经网络的激活函数,会被调用超过一亿次。根据 dotTrace 的数据,它占据了整个函数时间的约 60%。
你会如何对其进行优化?
public static float Sigmoid(double value) {
return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
我整天在对一个应用程序进行性能分析,优化了一些代码,但还剩下这个任务。这是神经网络的激活函数,会被调用超过一亿次。根据 dotTrace 的数据,它占据了整个函数时间的约 60%。
你会如何对其进行优化?
public static float Sigmoid(double value) {
return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
试试这样:
public static float Sigmoid(double value) {
return 1.0f / (1.0f + (float) Math.Exp(-value));
}
编辑: 我进行了一个快速的基准测试。在我的机器上,上述代码比你的方法快约43%,而这个数学上等价的代码略微更快一点(比原始代码快46%):
public static float Sigmoid(double value) {
float k = Math.Exp(value);
return k / (1.0f + k);
}
编辑2:我不确定C#函数有多大的开销,但是如果你在源代码中#include <math.h>
,则应该能够使用这个函数,它使用了一个浮点数指数函数。速度可能会更快。
public static float Sigmoid(double value) {
float k = expf((float) value);
return k / (1.0f + k);
}
另外,如果您要执行数百万次调用,则函数调用开销可能是一个问题。尝试创建一个内联函数,看看是否有帮助。
1 / (1+k)
还是 k / (1+k)
? - Aaron Franke如果这是针对激活函数的话,计算e^x是否完全准确其实并不那么重要?
例如,如果你使用近似值(1+x/256)^256,在我用Java测试的Pentium上(我假设C#本质上会编译为相同的处理器指令),这个近似值比e^x(Math.exp())快7-8倍,对于范围在+/-1.5左右的x值,精度可以保持在小数点后两位,并且在所述范围内有正确的数量级。(显然,要将一个数升至256次方,你需要对该数进行8次平方--不要使用Math.Pow!)在Java中:
double eapprox = (1d + x / 256d);
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
根据你想要的精度,不断将256加倍或减半(并添加/删除乘法)。即使n=4,对于-0.5到0.5之间的x值,它仍然可以提供约1.5个小数位的精度(并且比Math.exp()快15倍左右)。
附注:我忘了提到——显然不应该真正除以256:而是乘以一个常数1/256。Java的JIT编译器会自动进行优化(至少,Hotspot会),我假设C#也会这样做。
public static double Exp(double val) {
long tmp = (long) (1512775 * val + 1072632447);
return BitConverter.Int64BitsToDouble(tmp << 32);
}
(0.0007242, 0.0007376), (1.55306, 1.57713), (307.78015, 309.18896), (1093286.54660, 1050935.0), (9.76825E+30, 9.57295E+30)
。 - Special Sauce更新2:我删除了关于LUT的内容,因为我将其与完整的哈希混淆了。感谢Henrik Gustafsson让我回到正轨。所以内存不是问题,尽管搜索空间仍然会受到局部极值的影响。
当调用次数达到1亿次时,我会开始怀疑分析器开销是否会扭曲您的结果。请用一个无操作符替换计算,并查看它是否仍然报告占用60%的执行时间...
或者更好的方法是,创建一些测试数据并使用秒表计时器来分析100万次左右的调用。
如果您能与C++进行互操作,可以考虑将所有值存储在数组中,并使用SSE循环遍历它们,例如:
void sigmoid_sse(float *a_Values, float *a_Output, size_t a_Size){
__m128* l_Output = (__m128*)a_Output;
__m128* l_Start = (__m128*)a_Values;
__m128* l_End = (__m128*)(a_Values + a_Size);
const __m128 l_One = _mm_set_ps1(1.f);
const __m128 l_Half = _mm_set_ps1(1.f / 2.f);
const __m128 l_OneOver6 = _mm_set_ps1(1.f / 6.f);
const __m128 l_OneOver24 = _mm_set_ps1(1.f / 24.f);
const __m128 l_OneOver120 = _mm_set_ps1(1.f / 120.f);
const __m128 l_OneOver720 = _mm_set_ps1(1.f / 720.f);
const __m128 l_MinOne = _mm_set_ps1(-1.f);
for(__m128 *i = l_Start; i < l_End; i++){
// 1.0 / (1.0 + Math.Pow(Math.E, -value))
// 1.0 / (1.0 + Math.Exp(-value))
// value = *i so we need -value
__m128 value = _mm_mul_ps(l_MinOne, *i);
// exp expressed as inifite series 1 + x + (x ^ 2 / 2!) + (x ^ 3 / 3!) ...
__m128 x = value;
// result in l_Exp
__m128 l_Exp = l_One; // = 1
l_Exp = _mm_add_ps(l_Exp, x); // += x
x = _mm_mul_ps(x, x); // = x ^ 2
l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_Half, x)); // += (x ^ 2 * (1 / 2))
x = _mm_mul_ps(value, x); // = x ^ 3
l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver6, x)); // += (x ^ 3 * (1 / 6))
x = _mm_mul_ps(value, x); // = x ^ 4
l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver24, x)); // += (x ^ 4 * (1 / 24))
#ifdef MORE_ACCURATE
x = _mm_mul_ps(value, x); // = x ^ 5
l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver120, x)); // += (x ^ 5 * (1 / 120))
x = _mm_mul_ps(value, x); // = x ^ 6
l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver720, x)); // += (x ^ 6 * (1 / 720))
#endif
// we've calculated exp of -i
// now we only need to do the '1.0 / (1.0 + ...' part
*l_Output++ = _mm_rcp_ps(_mm_add_ps(l_One, l_Exp));
}
}
请记住,你将使用的数组应该使用_aligned_malloc(some_size * sizeof(float), 16)进行分配,因为SSE需要内存对齐到边界。
使用SSE,我可以在大约半秒钟内计算出所有1亿个元素的结果。然而,一次性分配那么多内存会花费你近2/3 GB的空间,因此我建议您每次处理更多但更小的数组。您甚至可能考虑使用双缓冲方式来处理100K个元素或更多。
此外,如果元素数量开始显著增长,您可能需要选择在GPU上处理这些内容(只需创建一个1D float4纹理并运行一个非常简单的片段着色器)。
以下是与已发布答案相关的C#基准测试结果。 (Empty是一个只返回0的函数,用于测量函数调用开销)
Empty Function: 79ms 0 原始方法: 1576ms 0.7202294 简化方法: (soprano) 681ms 0.7202294 近似方法: (Neil) 441ms 0.7198783 位操作: (martinus) 836ms 0.72318 泰勒展开式: (Rex Logan) 261ms 0.7202305 查找表法: (Henrik) 182ms 0.7204863
public static object[] Time(Func<double, float> f) {
var testvalue = 0.9456;
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 1e7; i++)
f(testvalue);
return new object[] { sw.ElapsedMilliseconds, f(testvalue) };
}
public static void Main(string[] args) {
Console.WriteLine("Empty: {0,10}ms {1}", Time(Empty));
Console.WriteLine("Original: {0,10}ms {1}", Time(Original));
Console.WriteLine("Simplified: {0,10}ms {1}", Time(Simplified));
Console.WriteLine("Approximate: {0,10}ms {1}", Time(ExpApproximation));
Console.WriteLine("Bit Manip: {0,10}ms {1}", Time(BitBashing));
Console.WriteLine("Taylor: {0,10}ms {1}", Time(TaylorExpansion));
Console.WriteLine("Lookup: {0,10}ms {1}", Time(LUT));
}
注意: 这是对此帖子的跟进。
现在看看你让我做的事情! 你让我安装Mono!
$ gmcs -optimize test.cs && mono test.exe
Max deviation is 0.001663983
10^7 iterations using Sigmoid1() took 1646.613 ms
10^7 iterations using Sigmoid2() took 237.352 ms
此方法中的一些问题:
抱歉复制粘贴代码...
using System;
using System.Diagnostics;
class LUTTest {
private const float SCALE = 320.0f;
private const int RESOLUTION = 2047;
private const float MIN = -RESOLUTION / SCALE;
private const float MAX = RESOLUTION / SCALE;
private static readonly float[] lut = InitLUT();
private static float[] InitLUT() {
var lut = new float[RESOLUTION + 1];
for (int i = 0; i < RESOLUTION + 1; i++) {
lut[i] = (float)(1.0 / (1.0 + Math.Exp(-i / SCALE)));
}
return lut;
}
public static float Sigmoid1(double value) {
return (float) (1.0 / (1.0 + Math.Exp(-value)));
}
public static float Sigmoid2(float value) {
if (value <= MIN) return 0.0f;
if (value >= MAX) return 1.0f;
if (value >= 0) return lut[(int)(value * SCALE + 0.5f)];
return 1.0f - lut[(int)(-value * SCALE + 0.5f)];
}
public static float error(float v0, float v1) {
return Math.Abs(v1 - v0);
}
public static float TestError() {
float emax = 0.0f;
for (float x = -10.0f; x < 10.0f; x+= 0.00001f) {
float v0 = Sigmoid1(x);
float v1 = Sigmoid2(x);
float e = error(v0, v1);
if (e > emax) emax = e;
}
return emax;
}
public static double TestPerformancePlain() {
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 10; i++) {
for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
Sigmoid1(x);
}
}
sw.Stop();
return sw.Elapsed.TotalMilliseconds;
}
public static double TestPerformanceLUT() {
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 10; i++) {
for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
Sigmoid2(x);
}
}
sw.Stop();
return sw.Elapsed.TotalMilliseconds;
}
static void Main() {
Console.WriteLine("Max deviation is {0}", TestError());
Console.WriteLine("10^7 iterations using Sigmoid1() took {0} ms", TestPerformancePlain());
Console.WriteLine("10^7 iterations using Sigmoid2() took {0} ms", TestPerformanceLUT());
}
}
F#在.NET数学算法中比C#表现更好。 因此,将神经网络重写为F#可能会提高整体性能。
如果我们在F#中重新实现LUT基准测试片段(我一直在使用稍微调整过的版本),则生成的代码:
有关详细信息,请参见博客文章。以下是F#片段,以防万一:
#light
let Scale = 320.0f;
let Resolution = 2047;
let Min = -single(Resolution)/Scale;
let Max = single(Resolution)/Scale;
let range step a b =
let count = int((b-a)/step);
seq { for i in 0 .. count -> single(i)*step + a };
let lut = [|
for x in 0 .. Resolution ->
single(1.0/(1.0 + exp(-double(x)/double(Scale))))
|]
let sigmoid1 value = 1.0f/(1.0f + exp(-value));
let sigmoid2 v =
if (v <= Min) then 0.0f;
elif (v>= Max) then 1.0f;
else
let f = v * Scale;
if (v>0.0f) then lut.[int (f + 0.5f)]
else 1.0f - lut.[int(0.5f - f)];
let getError f =
let test = range 0.00001f -10.0f 10.0f;
let errors = seq {
for v in test ->
abs(sigmoid1(single(v)) - f(single(v)))
}
Seq.max errors;
open System.Diagnostics;
let test f =
let sw = Stopwatch.StartNew();
let mutable m = 0.0f;
let result =
for t in 1 .. 10 do
for x in 1 .. 1000000 do
m <- f(single(x)/100000.0f-5.0f);
sw.Elapsed.TotalMilliseconds;
printf "Max deviation is %f\n" (getError sigmoid2)
printf "10^7 iterations using sigmoid1: %f ms\n" (test sigmoid1)
printf "10^7 iterations using sigmoid2: %f ms\n" (test sigmoid2)
let c = System.Console.ReadKey(true);
输出结果(使用F# 1.9.6.2 CTP进行发布编译,不使用调试器):
Max deviation is 0.001664
10^7 iterations using sigmoid1: 588.843700 ms
10^7 iterations using sigmoid2: 156.626700 ms
更新: 更新了基准测试使用10^7次迭代,以便与C语言的结果进行比较
更新2: 下面是来自同一台机器的C语言实现的性能结果,以作比较:
Max deviation is 0.001664
10^7 iterations using sigmoid1: 628 ms
10^7 iterations using sigmoid2: 157 ms
我能够想到的一个方法是通过滥用浮点数来近似指数,这篇论文详细介绍了该方法(点击右上角链接以获取PDF),但我不确定它是否对.NET有用。
此外,还有一点需要注意:为了快速训练大型网络,你正在使用的逻辑Sigmoid函数相当糟糕。请参阅LeCun等人的Efficient Backprop第4.4节,并使用一些零中心化的函数(实际上,请阅读整篇论文,它非常有用)。