为什么GCC不把a*a*a*a*a*a优化成(a*a*a)*(a*a*a)?

2302
我正在进行一些科学应用的数值优化。其中一个问题是,GCC会通过编译成a*a来优化调用pow(a,2),但是调用pow(a,6)不会被优化,并且实际上会调用库函数pow,这会大大降低性能。(相比之下,英特尔C++编译器,可执行文件icc,将消除对pow(a,6)的库调用。)
我很好奇的是,在使用了GCC 4.5.1和选项"-O3 -lm -funroll-loops -msse4"后,当我用a*a*a*a*a*a替换pow(a,6)时,它使用了5个mulsd指令:
movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

如果我写成 (a*a*a)*(a*a*a),它将会产生以下结果:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

这个技巧将乘法指令的数量减少到3。icc 有类似的行为。

为什么编译器不识别这个优化技巧呢?


16
“recognizing pow(a,6)” 的意思是“识别 a 的六次方”。 - Varun Madiath
707
嗯...你知道aaaaaa和(aaa)(aaa)在浮点数中不同,对吧?你需要使用-funsafe-math或-ffast-math等选项来解决这个问题。 - Damon
132
建议您阅读David Goldberg所写的《计算机科学家应该了解的浮点运算知识》(英文名:What Every Computer Scientist Should Know About Floating Point Arithmetic),链接为http://download.oracle.com/docs/cd/E19957-01/806-3568/ncg_goldberg.html。读完后,您将更加全面地理解自己刚刚踏入的泥潭! - Phil Armstrong
210
一个非常合理的问题。20年前我也问了同样的问题,通过消除那个瓶颈,将一个蒙特卡洛模拟的执行时间从21小时缩短到7小时。在此过程中,内部循环中的代码被执行了13万亿次,但这使得模拟结果可以在一夜之间完成。(请参见下面的答案) - user1899861
33
也许还可以加入(a*a)*(a*a)*(a*a),同样的乘法次数,但可能更精确。 - Rok Kralj
显示剩余6条评论
12个回答

2911

由于浮点数运算不满足结合律,在浮点数乘法中操作数的分组方式会对答案的数值精度产生影响。

因此,大多数编译器在重新排序浮点数计算时非常保守,除非它们可以确保答案不变,或者除非您告诉它们您不关心数值精度。例如:gcc 的 -fassociative-math 选项允许 gcc 重新关联浮点运算,甚至可以使用 -ffast-math 选项来更积极地权衡精度和速度。


16
是的。使用-ffast-math参数会进行这样的优化。这是个好主意!但由于我们的代码更注重精度而非速度,最好不要使用该参数。 - xis
25
我记得 C99 允许编译器进行这样的“不安全”FP优化,但除了 x87 之外的任何东西上,GCC 都会尽力遵循 IEEE 754 - 这不是“误差范围”,只有一个正确答案 - tc.
16
pow 的实现细节与此无关;这个回答甚至没有提到 pow - Stephen Canon
19
ICC默认允许重新关联。如果您想获得符合标准的行为,则需要使用ICC设置“-fp-model precise”。相对于重新关联,clang和gcc默认采用严格的符合性。 - Stephen Canon
66
@xis,实际上,-fassociative-math 不是不准确,只是 a*a*a*a*a*a(a*a*a)*(a*a*a) 是不同的。这与准确度无关,而是与标准符合性和严格可重复结果有关,例如在任何编译器上都可以得到相同的结果。浮点数已经不是精确的了。很少需要使用 -fassociative-math 进行编译。 - Paul Draper
显示剩余3条评论

710

Lambdageek正确指出,由于浮点数不满足结合律,将a*a*a*a*a*a优化为(a*a*a)*(a*a*a)可能会改变值。这就是为什么C99不允许它(除非用户通过编译器标志或pragma明确允许)。一般来说,假设程序员写下的代码有其原因,编译器应该尊重这个。如果你想要(a*a*a)*(a*a*a),那就直接写。

不过,这样写可能会很麻烦,为什么编译器不能在使用pow(a,6)时自动执行正确的操作呢?因为这样做是错误的。在具有良好数学库的平台上,pow(a,6)a*a*a*a*a*a或者(a*a*a)*(a*a*a)都要精确得多。仅提供一些数据,我在我的Mac Pro上进行了一个小实验,测量了所有单精度浮点数[1,2)之间计算a^6的最大误差:

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

使用pow函数代替乘法树可以将误差边界减少四倍。编译器不应该(通常也不会)进行增加误差的“优化”,除非用户经过许可(例如通过-ffast-math选项)。

注意,GCC提供了__builtin_powi(x,n)作为pow()的替代方案,它应该生成一个内联乘法树。如果您想在精度和性能之间做出权衡,但不想启用快速数学选项,请使用该函数。


33
还要注意,Visual C++ 提供了一个“增强”版的 pow() 函数。通过调用 _set_SSE2_enable(<flag>) 并将 flag=1,它将尽可能使用 SSE2。这会稍微降低精度,但会提高速度(在某些情况下)。MSDN:_set_SSE2_enable()pow() - TkTech
23
任何减少的精度都是由于 Microsoft 的实现问题,而不是使用寄存器的大小。如果库的编写者有动力,只使用32位寄存器就可以提供正确舍入的 pow 计算结果。有基于 SSE 的 pow 实现比大多数基于 x87 的实现更准确,还有一些实现为了速度而牺牲了一定的精度。 - Stephen Canon
12
当然,我只是想明确一下准确度降低是由库的编写者所做的选择造成的,而不是使用SSE本身固有的问题。 - Stephen Canon
10
我很想知道你在计算相对误差时使用的“黄金标准”是什么--通常我会认为它应该是a*a*a*a*a*a,但显然不是这种情况! :) - j_random_hacker
12
由于我在比较单精度结果,双精度足以成为黄金标准——在双精度计算的aaaaaa中产生的误差远远*小于任何单精度计算的误差。 - Stephen Canon
显示剩余9条评论

193

还有一个类似的情况:大多数编译器不会将a + b + c + d优化为(a + b) + (c + d)(这是一种优化,因为第二个表达式可以更好地进行管道处理)并按照给定的方式进行评估(即作为(((a + b) + c) + d))。这也是由于特殊情况造成的:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

这将输出 1.000000e-05 0.000000e+00


14
这并不完全相同。改变乘法/除法的顺序(除以0除外)比改变加法/减法的顺序更安全。在我看来,编译器应该尝试关联乘法/除法,因为这样可以减少总操作数,并且除了性能提升外,还有精度提升。 - CoffeDeveloper
10
@DarioOO:它并不更安全。乘除法与指数的加减法是相同的,改变顺序很容易导致临时值超出指数可能的范围。(虽然不完全相同,因为指数不会失去精度……但表示仍然相当有限,重新排序可能会导致无法表示的值) - Ben Voigt
13
我认为你缺少一些微积分的背景知识。乘除两个数字会引入相同数量的误差,而减法/加法两个数字可能会引入更大的误差,特别是当这两个数字相差很大时。因此,重新排列乘法/除法比进行加法/减法更安全,因为它会对最终误差产生较小的变化。 - CoffeDeveloper
12
风险在乘除运算中与加减运算不同:重新排列乘除运算对最终结果的影响微乎其微,或者指数会在某个点上溢出(在之前不会溢出),导致结果巨大地变化(可能为正无穷或0)。 - Peter Cordes
1
@GameDeveloper 在不可预测的方式中强制提高精度是非常棘手的问题。 - curiousguy
有趣的案例!由于所选的值,我们有a + b == a和c + d == c,当然a + c == 0,取决于添加所有4个变量的顺序,结果可能为0、1.000000e-05或2.000000e-05! - Claudiu Cruceanu

89
Fortran(专为科学计算而设计)具有内置的幂运算符,就我所知,Fortran编译器通常会像您所描述的那样优化整数幂的求解。然而,C/C++不幸地没有幂运算符,只有库函数pow()。这并不妨碍聪明的编译器特别处理pow,并以更快的方式计算特定情况下的幂,但似乎它们并不经常这样做...
几年前,我试图使计算整数幂更加方便和优化,并提出了以下解决方法。虽然它是C++,而不是C,但仍然取决于编译器在如何优化/内联事物方面有一定的智能。无论如何,希望您在实践中能够发现它有用:
template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

解释一下:这并不是找到计算幂的最优方法,但是由于找到最优解是NP完全问题,而且这只适用于小指数(与使用pow相比),所以没有必要过于关注细节。

然后只需使用power<6>(a)即可。

这使得输入幂变得容易(无需用括号拼写6个a),并且让您在需要精度依赖的情况下(例如补偿求和,这是一个需要操作顺序的示例)可以进行这种优化,而无需使用-ffast-math

您可能也可以忘记这是C++,并在C程序中使用它(如果它能够在C++编译器中编译通过)。

希望这对您有用。

编辑:

这是我从编译器得到的结果:

对于a*a*a*a*a*a

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

针对表达式(a*a*a)*(a*a*a)

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

对于power<6>(a)

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1

37
找到最优的幂树可能很困难,但由于只对小的幂感兴趣,显然的答案是预先计算一次(Knuth提供了一个表格,最高到100),并使用硬编码的表格(这就是gcc在powi内部所做的)。 - Marc Glisse
7
在现代处理器中,速度受延迟限制。例如,乘法的结果可能在五个周期后才能得到。在这种情况下,找到创建一些功率的最快方式可能会更加棘手。 - gnasher729
3
你还可以尝试找到能够给出相对舍入误差的最小上界或是最小平均相对舍入误差的幂树。 - gnasher729
1
Boost也支持这个功能,例如boost::math::pow<6>(n); 我认为它甚至会尝试通过提取公共因子来减少乘法的数量。 - gast128
请注意,最后一个等同于 (a**2)**3。 - minmaxavg
1
这是Fortran做出正确选择的情况之一(编译器可以使用结合性——除非用户使用括号,这是一种常用表示计算顺序的符号),而C作出了错误选择(没有办法进行关联数学运算)。 - tobi_s

73

当a为整数时,GCC实际上会将a*a*a*a*a*a优化为(a*a*a)*(a*a*a)。我已经尝试使用以下命令:

GCC实际上会将a*a*a*a*a*a优化为(a*a*a)*(a*a*a),当a是一个整数时。我用了这个命令进行测试:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

有很多gcc标志,但没有什么特别的。它们的含义是:从stdin读取;使用O2优化级别;输出汇编语言列表而不是二进制文件;列表应使用英特尔汇编语言语法;输入为C语言(通常语言是从输入文件扩展名中推断出来的,但是从stdin读取时没有文件扩展名);并写入stdout。

以下是输出的重要部分。我已经用一些注释对使用的汇编语言进行了说明:

; x is in edi to begin with.  eax will be used as a temporary register.
mov  eax, edi  ; temp = x
imul eax, edi  ; temp = x * temp
imul eax, edi  ; temp = x * temp
imul eax, eax  ; temp = temp * temp

我正在Linux Mint 16 Petra上使用系统GCC,这是Ubuntu的一个衍生版本。这是gcc的版本:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

正如其他帖子所指出的那样,这个选项在浮点数中是不可能的,因为浮点运算不是可结合的。


21
这是整数乘法的合法性,因为二进制补码溢出是未定义的行为。如果会发生溢出,无论如何重新排列操作,都将在某个地方发生。所以,没有溢出的表达式评估结果相同,溢出的表达式是未定义的行为,所以编译器可以改变溢出发生的时间点。GCC也对“unsigned int”执行此操作。 - Peter Cordes
3
我认为更好的合法原因是,与浮点数乘法不同,整数乘法(mod n)是可结合的。当然,有符号整数类型溢出仍然是未定义行为,但是假装它不是未定义的,你总是会从a*a*a*a*a*a(a*a*a)*(a*a*a)得到相同的结果。(当然,对于无符号类型溢出本来就不是未定义行为。) - Daniel McLaury
1
@DanielMcLaury:哦,是的,我没有明确说明那个关键要求。 :P 显然在2015年我认为每个人都已经知道了,或者是在确认实际整数结果相同之后可能会担心可能的UB问题。(另一方面,我记得看到过一个情况,即GCC没有像无符号整数那样优化有符号整数运算,因为某些过于保守的“不引入UB”的逻辑,在最终结果相同时这是没有意义的。) - Peter Cordes

51
因为32位浮点数(例如1.024)不等于1.024。在计算机中,1.024是一个区间:从(1.024-e)到(1.024+e),其中“e”表示误差。一些人没有意识到这一点,还认为a*a中的*代表对任意精度数字的乘法运算,而没有附加任何错误。一些人之所以没有意识到这一点,可能是因为他们在小学时进行的数学计算只涉及没有附加错误的理想数字,并且认为在执行乘法时可以简单地忽略“e”。他们没有看到“float a=1.2”,“a*a*a”和类似C代码中隐含的“e”。
如果大多数程序员能够认识到(并能够执行)C表达式a*a*a*a*a*a实际上并不使用理想数字,那么GCC编译器将可以将“a*a*a*a*a*a”优化为“t=(a*a); t*t*t”,这需要更少的乘法。但不幸的是,GCC编译器不知道编写代码的程序员是否认为“a”是带有或不带有误差的数字。因此,GCC将只做源代码的样子——因为这就是GCC用“肉眼”看到的。
... 一旦你知道自己是什么样的程序员,你可以使用“-ffast-math”开关告诉GCC“嘿,GCC,我知道我在做什么!”这将允许GCC将a*a*a*a*a*a转换为不同的文本——它看起来与a*a*a*a*a*a不同,但仍然计算出a*a*a*a*a*a的误差区间内的数字。这是可以接受的,因为你已经知道你正在处理的是区间而不是理想数字。

66
浮点数是精确的,只是不一定完全符合您的预期。此外,使用 epsilon 技巧本身就是一种近似处理方法,因为真实的期望误差相对于尾数的比例尺度而言,通常会出现大约 1 个 LSB 的偏差,但如果您不小心,在执行每次操作时都会增加这种偏差,所以在使用浮点数进行重要操作之前,请先咨询数值分析专家。如果可能的话,请使用正确的库。 - Donal Fellows
4
IEEE标准要求浮点计算的结果应该尽可能准确地反映出如果源操作数是精确值所得到的结果,但这并不意味着它们实际上表示精确值。在很多情况下,将0.1f视为(1,677,722 +/- 0.5)/16,777,216会更有帮助,应该显示出与该不确定性相对应的十进制数字位数,而不是将其视为精确数量(1,677,722 +/- 0.5)/16,777,216(应该显示24个十进制数字位数)。 - supercat
31
IEEE-754非常明确地指出浮点数据表示确切的值;第3.2 - 3.4条款是相关章节。当然,你可以选择以不同的方式解释它们,就像你可以选择将“int x = 3”解释为“x为3±0.5”的意思一样。 - Stephen Canon
8
我完全同意,但这并不意味着“距离”不恰好等于它的数值;这意味着该数值仅是对某个被建模的物理量的近似值。 - Stephen Canon
13
对于数值分析,如果你把浮点数解释为确切的值(恰好不是你想要的值),而不是区间,那么你的大脑会感谢你。例如,如果 x 大约是 4.5,误差小于 0.1,当你计算 (x + 1) - x 时,“区间”解释让你得到一个从 0.8 到 1.2 的区间,而“确切值”解释告诉你结果将是 1,双精度下误差最多为 2^(-50)。 - gnasher729
显示剩余3条评论

43

目前还没有海报提到浮点表达式的合并(ISO C标准,6.5p8和7.12.2)。如果FP_CONTRACT编译指示设置为ON,则编译器可以将a*a*a*a*a*a这样的表达式视为单个操作,就像使用单个舍入精确计算一样。例如,编译器可能会将其替换为内部幂函数,这既更快也更精确。这是特别有趣的,因为该行为在源代码中直接由程序员部分控制,而终端用户提供的编译器选项有时可能会被错误地使用。

FP_CONTRACT编译指示的默认状态是实现定义的,因此编译器可以默认进行此类优化。因此,需要严格遵循IEEE 754规则的可移植代码应将其显式设置为OFF

如果编译器不支持此编译指示,则必须谨慎处理,避免任何此类优化,以防开发人员选择将其设置为OFF

GCC不支持此编译指示,但在默认选项下,它会假定FP_CONTRACT设置为ON;因此,对于具有硬件FMA的目标,如果想要防止转换a*b+c为fma(a,b,c),则需要提供选项,例如-ffp-contract=off(将编译指示显式设置为OFF)或-std=c99(告诉GCC遵循某个C标准版本,这里是C99,从而遵循上述段落)。过去,后者的选项没有防止转换,这意味着GCC在这一点上不符合规范:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845


3
有些历史悠久的常见问题可能显得有些过时。这个问题是在2011年提出并回答的,那时GCC不能完全符合当时最新的C99标准可以被原谅。当然,现在已经是2014年了,所以GCC...咳咳。 - Pascal Cuoq
你不应该回答那些没有被接受的相对较新的浮点数问题吗?咳咳,https://dev59.com/TmAg5IYBdhLWcg3wWpxE 咳咳。 - Pascal Cuoq
我发现很不安,因为gcc没有实现C99浮点数编译指示。 - David Monniaux
1
@DavidMonniaux 根据定义,编译指示是可选的实现。 - Tim Seguine
2
@TimSeguine 但是,如果一个编译指示没有被实现,那么它的默认值需要对实现来说是最严格的。我想这就是David所考虑的。使用GCC,现在可以通过使用ISO C模式来修复FP_CONTRACT:它仍然没有实现编译指示,但在ISO C模式下,它现在假定编译指示已关闭。 - vinc17

32

类似"pow"的库函数通常被精心制作,以在一般情况下产生尽可能小的误差。通常使用样条曲线逼近函数来实现这一点(根据Pascal的评论,最常见的实现似乎是使用Remez算法)。

基本上是以下操作:

pow(x,y);

具有固有误差,其大小约与任何单个乘法或除法操作的误差相同。

以下操作:

float a=someValue;
float b=a*a*a*a*a*a;

对于一个包含5个乘法的运算,误差比单次乘除的误差更大5倍以上,这是其固有的误差。

编译器在进行优化时应该非常小心:

  1. 如果将pow(a,6)优化为a*a*a*a*a*a,可能会提高性能,但会严重降低浮点数的精度。
  2. 如果将a*a*a*a*a*a优化为pow(a,6),实际上可能会降低精度,因为"a"是一些特殊值,可以允许无误差地进行乘法(2的幂或某些小整数)。
  3. 如果将pow(a,6)优化为(a*a*a)*(a*a*a)(a*a)*(a*a)*(a*a),与pow函数相比仍然可能存在精度损失。

通常情况下,对于任意的浮点数值,使用"pow"函数的精度要优于自己编写任何函数的精度,但在某些特殊情况下,多次乘法可能具有更好的精度和性能。开发人员需要根据实际情况选择适合的方案,并在代码中进行注释,以避免其他人对该代码进行"优化"。

唯一有意义的(个人观点,并且似乎是GCC的一个选项,不需要特殊的优化或编译器标志)优化应该是将"pow(a,2)"替换为"a*a"。这应该是编译器厂商应该做的唯一明智的事情。


7
投下负评的人应该意识到这个答案是完全没问题的。我可以引用数十个来源和文献来支持我的答案,而且我对浮点精度的了解可能比任何一个投负评的人都要多。在StackOverflow上补充其他答案没有涉及到的信息是完全合理的,所以请礼貌地解释你的理由。 - CoffeDeveloper
3
在我看来,Stephen Canon的回答已经涵盖了你要说的内容。你似乎坚持认为libms是用样条实现的:它们更通常使用参数缩减(取决于正在实现的函数)加上一个单一的多项式,其系数已通过Remez算法的更或少复杂的变体获得。对于libm函数,连接点处的平滑度不被视为值得追求的目标(如果它们最终足够准确,无论域被分成多少个部分,它们都会自动变得相当平滑)。 - Pascal Cuoq
3
你的回答后半部分完全忽略了编译器应该生成实现源代码的代码,没有其他的。此外,当你使用“precision”一词时,你的意思是“accuracy”。 - Pascal Cuoq
感谢您的输入,我稍微修改了答案,在最后两行中仍然有一些新内容^^ - CoffeDeveloper

31

如Lambdageek所指出,浮点数乘法不满足结合律,因此可能会导致精度下降。但是,当需要更高的精度时,您可能会反对优化,因为您想要一个确定性应用程序。例如,在游戏模拟客户端/服务器中,每个客户端都必须模拟相同的世界,您希望浮点数计算具有确定性。


@Alice - 只有当编译器不会重新排列事物时,才可能以不同的方式根据编译器版本、目标机器等进行排序。 - greggo
3
不,这仍然是确定性的。在任何意义上都没有添加随机性。 - Alice
9
@Alice,Bjorn在这里使用“ deterministic ”一词似乎相当明确,即代码在不同平台和不同编译器版本等情况下会产生相同的结果(外部变量可能超出程序员的控制),而不是运行时缺乏实际数字随机性。如果你指出这不是该词的正确用法,我不会争论这一点。 - greggo
5
即使按照你对他说的话的解释,仍然是错误的;这就是IEEE 754的全部意义,为大多数(如果不是所有)操作在不同平台上提供相同的特性。现在,他没有提到平台或编译器版本,如果你想要每个远程服务器/客户端上的每个操作都完全相同,这将是一个有效的关注点......但这并不明显。更好的词可能是“可靠地相似”或其他类似的词语。 - Alice
8
@Alice,你在争论语义问题上浪费了每个人的时间,包括自己。他的意思很明确。 - Lanaru
12
标准的整个意义就在于语义;他的意思显然不够清晰。 - Alice

26

我原本并不指望这种情况能被优化。很少出现一个表达式中包含的子表达式可以重新组合以消除整个操作。我认为编译器开发者会把时间投入到更可能导致显著改进的领域,而不是涉及很少遇到的边缘情况。

我从其他答案中得知,只要使用正确的编译器选项,该表达式确实可以进行优化。要么这种优化很简单,要么这是一种更常见优化的边缘情况,或者编译器开发者非常勤奋。

像您在这里提供提示,让编译器进行优化并没有问题。将语句和表达式重新排列以查看它们带来的差异是微观优化过程的正常和预期部分。

尽管在没有正确选项的情况下,编译器可能有理由考虑两个表达式提供了不一致的结果,但您并不需要受到那种限制。差异将是微不足道的,如果这种微小的差异对您很重要,那么您首先就不应该使用标准浮点算法。


18
正如另一位评论者所指出的,这种说法不仅不正确,而且荒谬至极。精度差异可能高达成本的 0.5 到 10%,如果在紧密循环中运行,则将浪费许多指令来获得可以忽略不计的额外精度。说在进行 Monte Carlo 计算时不应使用标准 FP,有点像说你应该总是使用飞机穿越整个国家;它忽略了许多外部因素。最后,这并不是一个不常见的优化技巧;死代码分析和代码简化/重构非常普遍。 - Alice

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接