关于c#:Fast Exp计算:可以在不损失太多性能的情况下提高准确性吗?

Fast Exp calculation: possible to improve accuracy without losing too much performance?

我正在尝试快速的Exp(x)函数,该问题先前已在关于提高C#中的计算速度的SO问题的答案中进行了描述:

1
2
3
4
5
public static double Exp(double x)
{
  var tmp = (long)(1512775 * x + 1072632447);
  return BitConverter.Int64BitsToDouble(tmp << 32);
}

该表达式使用某些IEEE浮点"技巧",主要用于神经集合。该功能比常规Math.Exp(x)功能快大约5倍。

不幸的是,相对于常规Math.Exp(x)函数,数字精度仅为-4%-+ 2%,理想情况下,我希望精度至少在小于百分之一的范围内。

我已经画出了近似和常规Exp函数之间的商,从图中可以看出,相对差似乎是在几乎恒定的频率下重复出现的。

Quotient between fast and regular exp function

是否可以利用这种规律性来进一步提高"快速exp"功能的准确性,而又不会大大降低计算速度,或者准确性改善的计算开销会超过原始表达式的计算增益?

(作为一个旁注,我也尝试了在同一SO问题中提出的一种替代方法,但是这种方法在C#中似乎没有高效的计算能力,至少对于一般情况而言不是。)

5月14日更新

根据@Adriano的要求,我现在执行了一个非常简单的基准测试。对于浮点值在[-100,100]范围内的每个替代exp函数,我已经执行了1000万次计算。由于我感兴趣的值范围是从-20到0,因此我也明确列出了x = -5处的函数值。结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
      Math.Exp: 62.525 ms, exp(-5) = 0.00673794699908547
Empty function: 13.769 ms
     ExpNeural: 14.867 ms, exp(-5) = 0.00675211846828461
    ExpSeries8: 15.121 ms, exp(-5) = 0.00641270968867667
   ExpSeries16: 32.046 ms, exp(-5) = 0.00673666189488182
          exp1: 15.062 ms, exp(-5) = -12.3333325982094
          exp2: 15.090 ms, exp(-5) = 13.708332516253
          exp3: 16.251 ms, exp(-5) = -12.3333325982094
          exp4: 17.924 ms, exp(-5) = 728.368055056781
          exp5: 20.972 ms, exp(-5) = -6.13293614238501
          exp6: 24.212 ms, exp(-5) = 3.55518353166184
          exp7: 29.092 ms, exp(-5) = -1.8271053775984
      exp7 +/-: 38.482 ms, exp(-5) = 0.00695945286970704

ExpNeural等效于本文开头指定的Exp函数。我最初声称ExpSeries8是在.NET上效率不高的表述。当像Neil一样实现它时,它实际上非常快。 ExpSeries16是类似的公式,但具有16个乘法而不是8。exp1至exp7是与下面的Adriano答案不同的函数。 exp7的最终变体是检查x的符号的变体。如果为负,函数将返回1/exp(-x)

不幸的是,在我正在考虑的更广泛的负值范围内,Adriano列出的expN函数都不足够。尼尔·科菲(Neil Coffey)的级数展开方法似乎更适合"我"的值范围,尽管它与较大的负x太快发散了,特别是在使用"仅" 8乘法时。


尝试以下替代方法(exp1更快,exp7更精确)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static double exp1(double x) {
    return (6+x*(6+x*(3+x)))*0.16666666f;
}

public static double exp2(double x) {
    return (24+x*(24+x*(12+x*(4+x))))*0.041666666f;
}

public static double exp3(double x) {
    return (120+x*(120+x*(60+x*(20+x*(5+x)))))*0.0083333333f;
}

public static double exp4(double x) {
    return 720+x*(720+x*(360+x*(120+x*(30+x*(6+x))))))*0.0013888888f;
}

public static double exp5(double x) {
    return (5040+x*(5040+x*(2520+x*(840+x*(210+x*(42+x*(7+x)))))))*0.00019841269f;
}

public static double exp6(double x) {
    return (40320+x*(40320+x*(20160+x*(6720+x*(1680+x*(336+x*(56+x*(8+x))))))))*2.4801587301e-5;
}

public static double exp7(double x) {
  return (362880+x*(362880+x*(181440+x*(60480+x*(15120+x*(3024+x*(504+x*(72+x*(9+x)))))))))*2.75573192e-6;
}

精确

1
2
3
4
5
6
7
8
9
Function     Error in [-1...1]              Error in [3.14...3.14]

exp1         0.05           1.8%            8.8742         38.40%
exp2         0.01           0.36%           4.8237         20.80%
exp3         0.0016152      0.59%           2.28            9.80%
exp4         0.0002263      0.0083%         0.9488          4.10%
exp5         0.0000279      0.001%          0.3516          1.50%
exp6         0.0000031      0.00011%        0.1172          0.50%
exp7         0.0000003      0.000011%       0.0355          0.15%

积分
exp()的这些实现是通过" scoofy"使用" fuzzpilz"的tanh()实现的泰勒级数计算出来的(无论它们是什么,我的代码上都只有这些引用)。


泰勒级数逼近(例如Adriano答案中的expX()函数)最接近零,并且在-20或什至-5处可能有很大的误差。如果输入的范围是已知的,例如原始问题,例如-20到0,则可以使用一个小的查找表和一个附加的乘法来大大提高准确性。

诀窍是要认识到exp()可以分为整数和小数部分。例如:

1
exp(-2.345) = exp(-2.0) * exp(-0.345)

小数部分将始终在-1和1之间,因此泰勒级数逼近将非常准确。整数部分只有21个可能的值用于exp(-20)到exp(0),因此这些值可以存储在一个小的查询表中。


如果有人想复制问题中显示的相对误差函数,这是使用Matlab的一种方法("快速"指数在Matlab中不是很快,但是很准确):

1
2
3
4
5
6
7
t = 1072632447+[0:ceil(1512775*pi)];
x = (t - 1072632447)/1512775;
ex = exp(x);
t = uint64(t);
import java.lang.Double;
et = arrayfun( @(n) java.lang.Double.longBitsToDouble(bitshift(n,32)), t );
plot(x, et./ex);

现在,错误的周期与tmp的二进制值从尾数溢出到指数时正好重合。让我们通过丢弃成为指数的比特(使其成为周期性),并仅保留剩余的高8位(以使我们的查询表具有合理的大小),将数据划分为bin:

1
index = bitshift(bitand(t,uint64(2^20-2^12)),-12) + 1;

现在,我们计算所需的平均调整量:

1
2
3
4
relerrfix = ex./et;
adjust = NaN(1,256);
for i=1:256; adjust(i) = mean(relerrfix(index == i)); end;
et2 = et .* adjust(index);

相对误差减小到+/- .0006。当然,其他表大小也是可能的(例如,具有64个条目的6位表给出+/- .0025),并且表大小的误差几乎是线性的。表条目之间的线性插值会进一步改善错误,但会降低性能。由于我们已经达到了精度目标,因此我们避免进一步影响性能。

此时,掌握MatLab计算的值并在C#中创建查找表是一些琐碎的编辑器技能。对于每次计算,我们添加一个位掩码,一个表查找和一个双精度乘法。

1
2
3
4
5
6
static double FastExp(double x)
{
    var tmp = (long)(1512775 * x + 1072632447);
    int index = (int)(tmp >> 12) & 0xFF;
    return BitConverter.Int64BitsToDouble(tmp << 32) * ExpAdjustment[index];
}

加速与原始代码非常相似-对于我的计算机,以x86编译的速度大约快30%,而以x64编译的速度大约快3倍。单聚亚乙基二烯酮会造成可观的净亏损(但原始亏损也是如此)。

完整的源代码和测试用例:http://ideone.com/UwNgx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
using System;
using System.Diagnostics;

namespace fastexponent
{
    class Program
    {
        static double[] ExpAdjustment = new double[256] {
            1.040389835,
            1.039159306,
            1.037945888,
            1.036749401,
            1.035569671,
            1.034406528,
            1.033259801,
            1.032129324,
            1.031014933,
            1.029916467,
            1.028833767,
            1.027766676,
            1.02671504,
            1.025678708,
            1.02465753,
            1.023651359,
            1.022660049,
            1.021683458,
            1.020721446,
            1.019773873,
            1.018840604,
            1.017921503,
            1.017016438,
            1.016125279,
            1.015247897,
            1.014384165,
            1.013533958,
            1.012697153,
            1.011873629,
            1.011063266,
            1.010265947,
            1.009481555,
            1.008709975,
            1.007951096,
            1.007204805,
            1.006470993,
            1.005749552,
            1.005040376,
            1.004343358,
            1.003658397,
            1.002985389,
            1.002324233,
            1.001674831,
            1.001037085,
            1.000410897,
            0.999796173,
            0.999192819,
            0.998600742,
            0.998019851,
            0.997450055,
            0.996891266,
            0.996343396,
            0.995806358,
            0.995280068,
            0.99476444,
            0.994259393,
            0.993764844,
            0.993280711,
            0.992806917,
            0.992343381,
            0.991890026,
            0.991446776,
            0.991013555,
            0.990590289,
            0.990176903,
            0.989773325,
            0.989379484,
            0.988995309,
            0.988620729,
            0.988255677,
            0.987900083,
            0.987553882,
            0.987217006,
            0.98688939,
            0.98657097,
            0.986261682,
            0.985961463,
            0.985670251,
            0.985387985,
            0.985114604,
            0.984850048,
            0.984594259,
            0.984347178,
            0.984108748,
            0.983878911,
            0.983657613,
            0.983444797,
            0.983240409,
            0.983044394,
            0.982856701,
            0.982677276,
            0.982506066,
            0.982343022,
            0.982188091,
            0.982041225,
            0.981902373,
            0.981771487,
            0.981648519,
            0.981533421,
            0.981426146,
            0.981326648,
            0.98123488,
            0.981150798,
            0.981074356,
            0.981005511,
            0.980944219,
            0.980890437,
            0.980844122,
            0.980805232,
            0.980773726,
            0.980749562,
            0.9807327,
            0.9807231,
            0.980720722,
            0.980725528,
            0.980737478,
            0.980756534,
            0.98078266,
            0.980815817,
            0.980855968,
            0.980903079,
            0.980955475,
            0.981017942,
            0.981085714,
            0.981160303,
            0.981241675,
            0.981329796,
            0.981424634,
            0.981526154,
            0.981634325,
            0.981749114,
            0.981870489,
            0.981998419,
            0.982132873,
            0.98227382,
            0.982421229,
            0.982575072,
            0.982735318,
            0.982901937,
            0.983074902,
            0.983254183,
            0.983439752,
            0.983631582,
            0.983829644,
            0.984033912,
            0.984244358,
            0.984460956,
            0.984683681,
            0.984912505,
            0.985147403,
            0.985388349,
            0.98563532,
            0.98588829,
            0.986147234,
            0.986412128,
            0.986682949,
            0.986959673,
            0.987242277,
            0.987530737,
            0.987825031,
            0.988125136,
            0.98843103,
            0.988742691,
            0.989060098,
            0.989383229,
            0.989712063,
            0.990046579,
            0.990386756,
            0.990732574,
            0.991084012,
            0.991441052,
            0.991803672,
            0.992171854,
            0.992545578,
            0.992924825,
            0.993309578,
            0.993699816,
            0.994095522,
            0.994496677,
            0.994903265,
            0.995315266,
            0.995732665,
            0.996155442,
            0.996583582,
            0.997017068,
            0.997455883,
            0.99790001,
            0.998349434,
            0.998804138,
            0.999264107,
            0.999729325,
            1.000199776,
            1.000675446,
            1.001156319,
            1.001642381,
            1.002133617,
            1.002630011,
            1.003131551,
            1.003638222,
            1.00415001,
            1.004666901,
            1.005188881,
            1.005715938,
            1.006248058,
            1.006785227,
            1.007327434,
            1.007874665,
            1.008426907,
            1.008984149,
            1.009546377,
            1.010113581,
            1.010685747,
            1.011262865,
            1.011844922,
            1.012431907,
            1.013023808,
            1.013620615,
            1.014222317,
            1.014828902,
            1.01544036,
            1.016056681,
            1.016677853,
            1.017303866,
            1.017934711,
            1.018570378,
            1.019210855,
            1.019856135,
            1.020506206,
            1.02116106,
            1.021820687,
            1.022485078,
            1.023154224,
            1.023828116,
            1.024506745,
            1.025190103,
            1.02587818,
            1.026570969,
            1.027268461,
            1.027970647,
            1.02867752,
            1.029389072,
            1.030114973,
            1.030826088,
            1.03155163,
            1.032281819,
            1.03301665,
            1.033756114,
            1.034500204,
            1.035248913,
            1.036002235,
            1.036760162,
            1.037522688,
            1.038289806,
            1.039061509,
            1.039837792,
            1.040618648
        };

        static double FastExp(double x)
        {
            var tmp = (long)(1512775 * x + 1072632447);
            int index = (int)(tmp >> 12) & 0xFF;
            return BitConverter.Int64BitsToDouble(tmp << 32) * ExpAdjustment[index];
        }

        static void Main(string[] args)
        {
            double[] x = new double[1000000];
            double[] ex = new double[x.Length];
            double[] fx = new double[x.Length];
            Random r = new Random();
            for (int i = 0; i < x.Length; ++i)
                x[i] = r.NextDouble() * 40;

            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int j = 0; j < x.Length; ++j)
                ex[j] = Math.Exp(x[j]);
            sw.Stop();
            double builtin = sw.Elapsed.TotalMilliseconds;
            sw.Reset();
            sw.Start();
            for (int k = 0; k < x.Length; ++k)
                fx[k] = FastExp(x[k]);
            sw.Stop();
            double custom = sw.Elapsed.TotalMilliseconds;

            double min = 1, max = 1;
            for (int m = 0; m < x.Length; ++m) {
                double ratio = fx[m] / ex[m];
                if (min > ratio) min = ratio;
                if (max < ratio) max = ratio;
            }

            Console.WriteLine("minimum ratio =" + min.ToString() +", maximum ratio =" + max.ToString() +", speedup =" + (builtin / custom).ToString());
         }
    }
}


以下代码应满足精度要求,因为对于[-87,88]中的输入,结果的相对误差<= 1.73e-3。我不知道C#,所以这是C代码,但是我认为转换应该很简单。

我认为由于精度要求较低,因此使用单精度计算就可以了。正在使用经典算法,其中将exp()的计算映射到exp2()的计算。通过对数乘以log2(e)进行自变量转换后,使用2的极小极大多项式来处理小数部分的幂,而通过直接操纵IEEE-754单值的幂部分来执行自变量整数部分的幂。精度数。

易失性并集便于将位模式重新解释为指数操作所需的整数或单精度浮点数。看起来C#为此提供了确定的重新解释功能,这更加干净。

两个潜在的性能问题是floor()函数和float-> int转换。传统上,由于需要处理动态处理器状态,两者在x86上的运行速度都很慢。但是SSE(尤其是SSE 4.1)提供了允许这些操作快速进行的说明。我不知道C#是否可以利用这些指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 /* max. rel. error <= 1.73e-3 on [-87,88] */
 float fast_exp (float x)
 {
   volatile union {
     float f;
     unsigned int i;
   } cvt;

   /* exp(x) = 2^i * 2^f; i = floor (log2(e) * x), 0 <= f <= 1 */
   float t = x * 1.442695041f;
   float fi = floorf (t);
   float f = t - fi;
   int i = (int)fi;
   cvt.f = (0.3371894346f * f + 0.657636276f) * f + 1.00172476f; /* compute 2^f */
   cvt.i += (i << 23);                                          /* scale by 2^i */
   return cvt.f;
 }


我研究了Nicol Schraudolph的论文,其中更详细地定义了上述函数的原始C实现。似乎确实不可能在不严重影响性能的情况下实质上批准exp计算的准确性。另一方面,该近似对于x的大幅度也是有效的,最大为+/- 700,这当然是有利的。

调整上面的函数实现以获得最小的均方根误差。 Schraudolph描述了如何更改tmp表达式中的加法项以实现替代的近似性质。

1
2
3
4
5
"exp">= exp for all x                      1072693248 -  (-1) = 1072693249
"exp" <= exp for all x                                 - 90253 = 1072602995
"exp" symmetric around exp                             - 45799 = 1072647449
Mimimum possible mean deviation                        - 68243 = 1072625005
Minimum possible root-mean-square deviation            - 60801 = 1072632447

他还指出,在"微观"级别上,近似" exp"函数表现出阶梯行为,因为从长到双的转换中丢弃了32位。这意味着函数在很小的范围内是分段常数,但是函数至少永远不会随着x的增加而减小。