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浮点"技巧",主要用于神经集合。该功能比常规
不幸的是,相对于常规
我已经画出了近似和常规Exp函数之间的商,从图中可以看出,相对差似乎是在几乎恒定的频率下重复出现的。
是否可以利用这种规律性来进一步提高"快速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的符号的变体。如果为负,函数将返回
不幸的是,在我正在考虑的更广泛的负值范围内,Adriano列出的expN函数都不足够。尼尔·科菲(Neil Coffey)的级数展开方法似乎更适合"我"的值范围,尽管它与较大的负x太快发散了,特别是在使用"仅" 8乘法时。
尝试以下替代方法(
码
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% |
积分
泰勒级数逼近(例如Adriano答案中的
诀窍是要认识到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); |
现在,错误的周期与
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的增加而减小。