我的编译器会忽略无用的代码吗?

40

我在网络上看了一些与这个主题相关的问题,但是没有找到我的问题的答案,或者是针对另一种语言,或者是没有完全回答(死代码不是无用代码),所以这是我的问题:

(显式或隐式的)无用代码是否被编译器忽略?

例如,在这段代码中:

double[] TestRunTime = SomeFunctionThatReturnDoubles;
// A bit of code skipped
int i = 0;
for (int j = 0; j < TestRunTime.Length; j++)
{

}
double prevSpec_OilCons = 0;

for循环会被移除吗?

我使用


背景是我维护了很多代码(不是我写的),我想知道无用的代码是否应该成为一个目标,或者我可以让编译器来处理它。


4
不会。由于某些不明原因,如果不是数组,.Length 可能会产生副作用。 - leppie
13
编译它,查看在ILSpy/Reflector中的exe文件。 - Alex K.
2
@leppie 任何属性都可能具有副作用,因此优化编译器不得将其删除。 - Matthew
4
@AlexK,那只会告诉你C#编译器的操作,但是当涉及到优化时,JIT编译器的操作更为重要。 - svick
8
无用的代码应该成为另一个目标——可读性。无用的代码是世界上最令人困惑的一些代码,因为当我们处理代码时,我们试图弄清它在做什么,而且我们假设它确实在做某些事情。 - Benjamin Gruenbaum
显示剩余9条评论
9个回答

27
如果您的变量i和prevSpec_OilCons没有在其他地方使用,它们会被优化掉,但循环不会被优化。 因此,如果您的代码如下所示:
static void Main(string[] args)
{
    int[] TestRunTime = { 1, 2, 3 };
    int i = 0;
    for (int j = 0; j < TestRunTime.Length; j++)
    {

    }
    double prevSpec_OilCons = 0;
    Console.WriteLine("Code end");
}

ILSpy 下,它将是:

private static void Main(string[] args)
{
    int[] TestRunTime = new int[]
    {
        1,
        2,
        3
    };
    for (int i = 0; i < TestRunTime.Length; i++)
    {
    }
    Console.WriteLine("Code end");
}
由于循环包含比较和增量等多个语句,因此可以用于实现 稍微 短暂的延迟/等待时间。尽管这样做不是一个好习惯。 考虑以下循环,它是一个空循环,但执行起来需要很长时间。
for (long j = 0; j < long.MaxValue; j++)
{

}
您的代码中的循环不是死代码,就死代码而言,以下是死代码,将被优化掉。
if (false)
{
    Console.Write("Shouldn't be here");
}

这个循环甚至不会被.NET jitters移除。基于此答案


8
如果执行时间是一个理想的特性,编译器将不允许进行性能优化,因此“...它可以用于实现相对较短的延迟/等待时间…” - Stefan Steinegger
1
现在,他正在循环属性,当调用它们时可能会执行一些内部操作。如果他硬编码一个int字面量,而不是TestRunTime.Lengthlong.MaxValue,那么它还会保持for循环吗? - Panzercrisis
1
@Panzercrisis,是的,我在发布答案之前在ILSpy中检查了它,所以for (long j = 0; j < long.MaxValue; j++) {}将留在代码中,不会被优化去除。 - Habib
1
我的意思是,如果他说 for (long j = 0; j < 3; j++){} 呢?我想它可能仍然存在,但我猜可能有一点小概率。 - Panzercrisis
3
@Panzercrisis,它在那里,我可以在ILSpy中看到。 - Habib
显示剩余4条评论

15

循环无法移除,代码不是死的,例如:

  // Just some function, right?
  private static Double[] SomeFunctionThatReturnDoubles() {
    return null;
  }

  ...
  double[] TestRunTime = SomeFunctionThatReturnDoubles();
  ...
  // You'll end up with exception since TestRunTime is null
  for (int j = 0; j < TestRunTime.Length; j++)
  {
  }
  ...

通常,编译器只是无法预测SomeFunctionThatReturnDoubles的所有可能结果,这就是为什么它保留循环的原因。


1
C#编译器或JIT都可以合法地将循环优化为int len = TestRunTime.Length; for (int j=0; j<len; j++) {}(即使对于JIT来说,这也是一个简单的优化);从那里,它可以轻松地优化为TestRunTime.Length;。虽然没有完全消除它,但可以使其运行速度更快。 - supercat
2
@supercat 这不一样。你不能确定 Length 属性没有其他作用。例如,如果 Length 跟踪在返回结果之前调用 get {} 的次数,则您的优化将与实际预期不同。编译器必须绝对确定没有副作用才能进行优化。 - Drakarah
1
@drake7707:对于除了数组以外的类型,这是正确的。 C# 和 Runtime 都知道什么是数组;对于单维基于零的数组,访问元素的生成机器码不会调用“访问元素”子程序,而是检查索引是否超出界限,如果越界则抛出异常,否则直接获取数据。此外,如果 JIT 可以确定循环内将访问的最大下标,它可以根据我所读的内容在循环之前检查长度,并且如果可接受,则在循环中省略检查。 - supercat
@drake7707:如果JIT可以提升用于下标验证的数组边界测试,我认为它同样可以提升由其他原因引起的长度请求。 - supercat
@supercat 哦,是的,我忘记了数组边界检查优化。好吧,我想如果编译器能够从 IL 中推断出它是纯只读的(例如没有 stloc 或 stfld 等),我看不出为什么它不能(在这种情况下为什么不应该)优化上面的片段。 - Drakarah

6

在您的循环中,每次迭代都有两个操作。其中一个是递增:

j++;

和比较

j<TestRunTime.Length;
所以,尽管看起来循环为空,但实际上仍在执行某些操作,编译器当然不会忽略这些操作。其他循环也会出现这种情况。

4

这段代码不会被忽略。然而,当程序运行到IL代码时,会出现一个跳转语句,所以 for 循环会像 if 语句一样运行。同时,它也会运行 ++ 和 length 的代码,就像 @Fleve 所提到的那样。只是多了一些无用的代码。为了可读性和代码规范,如果你不使用这段代码,最好将其删除。


4
是否(显式或隐式的)无用代码会被编译器忽略? 你不能轻易地确定它是否是无用的,所以编译器也不能。例如,TestRunTime.Length 的getter可能具有副作用。 背景是我维护了很多代码(不是我写的),我想知道无用的代码是否应该成为一个目标。 在重构代码之前,必须验证其功能,以便能够更改代码并确保其仍具有相同的结果。单元测试是实现此目标的好方法。

Double[].Length 有副作用吗? - Thomas Ayoub
1
自己找出答案:什么是“副作用”Array.(get_)Length没有副作用,它只返回数组的长度。 - CodeCaster
很好的链接,所以它不会。谢谢。 - Thomas Ayoub

4
JIT基本能够清除死代码,但清理程度不高。死变量和表达式会被可靠清除。这是SSA形式中的一项简单优化。 对于控制流,不确定情况。如果嵌套两个循环,我记得只有内部循环会被删除。 如果你想确切地知道什么被删除了,什么没有,可以查看生成的x86代码。C#编译器进行的优化非常少。JIT进行了一些优化。 4.5 32位和64位JIT是不同的代码基础,并且具有不同的行为。新的JIT(RyuJIT)即将推出,在我的测试中通常表现更差,有时则更好。

4
显然,编译器不会忽略无用的代码,而是会仔细分析并尝试删除它,如果进行了优化。 在您的情况下,第一件有趣的事情是变量j是否在循环后使用。另一个有趣的事情是TestRunTime.Length。编译器会查看它并检查它是否总是返回相同的结果,如果是,则检查它是否有任何副作用,如果有,则检查调用它一次是否与重复调用它产生相同的副作用。 如果TestRunTime.Length没有副作用且未使用j,则删除循环。 否则,如果多次调用TestRunTime.Length具有比一次调用更多的副作用,或者如果重复调用返回不同的值,则必须执行循环。 否则,j = max(0, TestRunTime.Length)。 接下来,编译器可以确定是否需要赋值TestRunTime.Length。它可以被替换为仅确定TestRunTime.Length将是什么的代码。 当然,您的编译器可能不会尝试任何花哨的优化,或者语言规则可能如此,以至于它无法确定这些内容,那么您就被卡住了。

1
变量j在循环后是否被使用?不可能,它是在循环内声明的(或者我读错了一些东西)。 - Thomas Ayoub
不需要,看下面我的回答... - Thomas Ayoub

3
大多数情况下,你不用担心主动去除无用的代码。如果你遇到性能问题,并且你的分析器表明一些无用的代码正在消耗你的时钟周期,那么就彻底清理掉它。然而,如果这段代码真的没有任何作用并且没有副作用,那么它对运行时间的影响可能很小。 话虽如此,大多数编译器不需要执行任何优化,因此依赖编译器的优化并不总是明智的选择。但在许多情况下,即使一个无用的自旋循环也可以执行得非常快。一个基本的自旋锁循环一百万次会被编译成类似于mov eax, 0 \ inc eax \ cmp eax, 1000000 \ jnz -8的东西。即使我们不考虑CPU上的优化,每个循环只需要三个周期(在最近的RISC风格芯片上),因为没有内存访问,所以不会有任何缓存失效。在 1GHz CPU 上,只需要 3,000,000/1,000,000,000 秒或者说 3 毫秒。如果你尝试以 60 帧每秒的速度运行它,那将是一个相当大的击打,但在许多情况下,它可能甚至不会被注意到。 像我描述的这样的循环几乎肯定会被优化成mov eax 1000000,即使在JIT环境下也是如此。它可能会被进一步优化,但在没有其他上下文的情况下,这种优化是合理的,并且不会造成任何负面影响。 简而言之:如果你的分析器表明死代码/无用代码使用了可观的运行时资源,请将其删除。但不要去寻找死代码,可以将其留给后续的重写/大规模重构。 附加内容:如果代码生成器知道eax除了循环条件以外不会被读取,而且想保留自旋锁,那么它可以生成mov eax, 1000000 \ dec eax \ jnz -3并将循环的惩罚降低一个周期。但大多数编译器都会将其完全删除。

我不喜欢“你有快速的CPU,所以不用担心,让它保持原样”的想法。缺乏微观优化会导致需要更快的处理器、更多的RAM等。“但这没关系”。如果一个人使用旧电脑,内存很少,那么为什么不帮助他呢?因为这不值得付出努力吗?我相信许多程序可以更小,适应旧电脑,但当然需要程序员的努力。出于某种原因,我们期望每隔几年就扔掉一台电脑并换成新的。为什么?Commodore 64只有64K RAM,甚至还有自己的办公套件。 - Voitcus
去年我从事视频解码工作,注重所有这些小细节,“这并不重要”的注意事项。我从播放高清@ ~55fps转变为流畅播放4K @ 60fps。所以,是的,这确实很重要。 - Thomas Ayoub
3
我会说,我做一些嵌入式开发,并坚定地认为微小的优化是有益的。我还说过:"如果你试图每秒运行60次,那将是一个相当大的损失",这是直接承认,如果您正在进行重要的数据处理,例如视频编解码或游戏逻辑,则任何(微小的)优化都很重要。答案更针对提问者,其中性能似乎不是一个重要问题,因为这段代码已经积累在代码库中,花费人力修复它可能不是一个选项。 - Kaslai

1

我根据一些答案者的建议,制作了一个小表格来测试它是否使用long.MaxValue,下面是我的参考代码:

public Form1()
{
    InitializeComponent();
    Stopwatch test = new Stopwatch();
    test.Start();
    myTextBox.Text = test.Elapsed.ToString();
}

以下是含有一些“没什么用”的代码:

public Form1()
{
    InitializeComponent();
    Stopwatch test = new Stopwatch();
    test.Start();
    for (int i = 0; i < int.MaxValue; i++)
    {
    }
    myTextBox.Text = test.Elapsed.ToString();
}

你会注意到我使用了int.MaxValue而不是long.MaxValue,我不想在这个问题上花费一年一天的时间。

正如你所看到的:

---------------------------------------------------------------------
|                   |   Debug               |   Release             |
---------------------------------------------------------------------
|Ref                |   00:00:00.0000019    |   00:00:00.0000019    |
|Useless code       |   00:00:05.3837568    |   00:00:05.2728447    |
---------------------------------------------------------------------
代码没有进行优化。稍等一下,我会用一些int[]来测试int[].Length
public Form1()
{
    InitializeComponent();
    int[] myTab = functionThatReturnInts(1);

    Stopwatch test = new Stopwatch();
    test.Start();
    for (int i = 0; i < myTab.Length; i++)
    {

    }
    myTextBox.Text = test.Elapsed.ToString();
}
public int[] functionThatReturnInts(int desiredSize)
{
    return Enumerable.Repeat(42, desiredSize).ToArray();
}

这是结果:

---------------------------------------------
|   Size            |   Release             |
---------------------------------------------
|             1     |   00:00:00.0000015    |
|           100     |   00:00:00            |
|        10 000     |   00:00:00.0000035    |
|     1 000 000     |   00:00:00.0003236    |
|   100 000 000     |   00:00:00.0312673    |
---------------------------------------------

所以即使使用数组,它也完全没有被优化。


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