我的编译器会忽略无用的代码吗?
Will my compiler ignore useless code?
我已经通过网络回答了几个关于这个主题的问题,但我没有找到我的问题的任何答案,或者它是for another language or it doesn't answer totally(死代码是不是 无用的代码)所以这是我的问题:
编译器是否忽略(显式或非显式)无用代码?
例如,在这段代码中:
double[] TestRunTime = SomeFunctionThatReturnDoubles;
// A bit of code skipped
int i = 0;
for (int j = 0; j < TestRunTime.Length; j++)
{
}
double prevSpec_OilCons = 0;
for 循环会被移除吗?
背景是我维护了很多代码(我没有写),我想知道无用的代码是否应该成为目标,或者我是否可以让编译器会处理这个问题。
在您的循环中,每次迭代中都隐含了两个操作。增量:
j++;
与比较
j<TestRunTime.Length;
所以,循环不是空的,尽管它看起来是空的。最后有一些东西正在执行,这当然不会被编译器忽略。
这也发生在其他循环中。
它不会被忽略。然而,当你到达 IL 时会有一个跳转语句,所以 for 将 运行 就好像它是一个 if 语句一样。正如@Fleve 提到的,它还将 运行 ++ 和长度的代码。这只是额外的代码。为了可读性以及与代码标准保持一致,如果您不使用代码,我会删除它。
好吧, 你的变量 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");
}
由于循环有几个语句,如比较和增量,它可以用于实现 有点 短 delay/wait 周期。 (虽然这样做不是一个好习惯)。
考虑下面的循环,这是一个空循环,但是执行起来会花费很多时间。
for (long j = 0; j < long.MaxValue; j++)
{
}
你代码中的循环,不是死码,就死码而言,下面是死码,会被优化掉。
if (false)
{
Console.Write("Shouldn't be here");
}
循环,甚至不会被 .NET 抖动消除。基于此answer
Is (explicit or not) useless code ignored by the compiler?
您不能轻易确定它没用,因此编译器也不能。例如,TestRunTime.Length
的 getter 可能会有副作用。
The background is that I maintain a lot of code (that I didn't write) and I was wondering if useless code should be a target
在你重构一段代码之前,你必须验证它做了什么才能改变它并在之后说它仍然有相同的结果。单元测试是执行此操作的好方法。
JIT 基本上可以去除死代码。它不是很彻底。死变量和表达式被可靠地杀死。这是 SSA 形式的简单优化。
不确定控制流。如果嵌套两个循环,我记得只有内部循环会被删除。
如果您想确定哪些已删除,哪些未删除,请查看生成的 x86 代码。 C# 编译器做的优化很少。 JIT 做了一些。
4.5 32 位和 64 位 JIT 是不同的代码库,具有不同的行为。一个新的 JIT (RyuJIT) 即将推出,在我的测试中通常表现更差,有时更好。
循环不能去掉,代码是没死,例如:
// 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
所有可能的结果,这就是为什么它保留循环
显然你的编译器不会忽略无用的代码,但会仔细分析它然后尝试删除它,如果它执行优化。
在你的例子中,第一个有趣的事情是变量 j 是否在循环之后使用。另一个有趣的事情是TestRunTime.Length。编译器会查看它并检查它是否总是 return 相同的结果,如果是,它是否有任何副作用,如果是,调用它一次是否与重复调用它具有相同的副作用。
如果 TestRunTime.Length 没有副作用并且不使用 j 则循环被删除。
否则,如果重复调用TestRunTime.Length比调用一次有更多的副作用,或者如果重复调用return不同的值,则必须执行循环。
否则,j = max (0, TestRunTime.Length)。
接下来,编译器可以判断是否需要赋值TestRunTime.Length。它可能会被仅确定 TestRunTime.Length 是什么的代码所取代。
当然你的编译器可能不会尝试任何花哨的优化,或者语言规则可能无法确定这些东西,你就被卡住了。
在大多数情况下,您不必担心主动删除无用代码。如果你 运行 遇到性能问题,并且你的分析器说一些无用的代码正在占用你的时钟周期,那么就去解决它。但是,如果代码真的什么都不做也没有副作用,那么它可能对 运行宁时间影响不大。
也就是说,大多数编译器不需要执行任何优化,因此依赖编译器优化并不总是最明智的选择。但在许多情况下,即使是无用的自旋循环也可以执行得非常快。循环一百万次的基本自旋锁会编译成类似 mov eax, 0 \ inc eax \ cmp eax, 1000000 \ jnz -8
的东西。即使我们不考虑 on-CPU 优化,每个循环也只有 3 个周期(在最近的 RISC 样式芯片上),因为没有内存访问,所以不会有任何缓存失效。在 1GHz CPU 上,只有 3,000,000/1,000,000,000 秒,即 3 毫秒。如果您尝试 运行每秒 60 次,那将是一个非常重要的打击,但在许多情况下,它甚至可能不会引人注意。
即使在 JIT 环境中,像我描述的那样的循环几乎肯定会针对 mov eax 1000000
进行窥孔优化。它可能会进一步优化,但在没有其他上下文的情况下,优化是合理的并且不会造成不良影响。
tl;dr:如果您的探查器说 dead/useless 代码正在使用您的 运行 时间资源的明显数量,请将其删除。不过,不要继续 寻找死代码;将其留给 rewrite/massive 重构。
好处:如果代码生成器知道 eax
除了循环条件之外不会被读取,并且想要保留自旋锁,它可以生成 mov eax, 1000000 \ dec eax \ jnz -3
并减少一个循环的循环惩罚。不过,大多数编译器会完全删除它。
我根据一些回答者关于使用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[].Lenght
:
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 |
---------------------------------------------
所以即使使用数组,它也根本没有得到优化。
我已经通过网络回答了几个关于这个主题的问题,但我没有找到我的问题的任何答案,或者它是for another language or it doesn't answer totally(死代码是不是 无用的代码)所以这是我的问题:
编译器是否忽略(显式或非显式)无用代码?
例如,在这段代码中:
double[] TestRunTime = SomeFunctionThatReturnDoubles;
// A bit of code skipped
int i = 0;
for (int j = 0; j < TestRunTime.Length; j++)
{
}
double prevSpec_OilCons = 0;
for 循环会被移除吗?
背景是我维护了很多代码(我没有写),我想知道无用的代码是否应该成为目标,或者我是否可以让编译器会处理这个问题。
在您的循环中,每次迭代中都隐含了两个操作。增量:
j++;
与比较
j<TestRunTime.Length;
所以,循环不是空的,尽管它看起来是空的。最后有一些东西正在执行,这当然不会被编译器忽略。
这也发生在其他循环中。
它不会被忽略。然而,当你到达 IL 时会有一个跳转语句,所以 for 将 运行 就好像它是一个 if 语句一样。正如@Fleve 提到的,它还将 运行 ++ 和长度的代码。这只是额外的代码。为了可读性以及与代码标准保持一致,如果您不使用代码,我会删除它。
好吧, 你的变量 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");
}
由于循环有几个语句,如比较和增量,它可以用于实现 有点 短 delay/wait 周期。 (虽然这样做不是一个好习惯)。
考虑下面的循环,这是一个空循环,但是执行起来会花费很多时间。
for (long j = 0; j < long.MaxValue; j++)
{
}
你代码中的循环,不是死码,就死码而言,下面是死码,会被优化掉。
if (false)
{
Console.Write("Shouldn't be here");
}
循环,甚至不会被 .NET 抖动消除。基于此answer
Is (explicit or not) useless code ignored by the compiler?
您不能轻易确定它没用,因此编译器也不能。例如,TestRunTime.Length
的 getter 可能会有副作用。
The background is that I maintain a lot of code (that I didn't write) and I was wondering if useless code should be a target
在你重构一段代码之前,你必须验证它做了什么才能改变它并在之后说它仍然有相同的结果。单元测试是执行此操作的好方法。
JIT 基本上可以去除死代码。它不是很彻底。死变量和表达式被可靠地杀死。这是 SSA 形式的简单优化。
不确定控制流。如果嵌套两个循环,我记得只有内部循环会被删除。
如果您想确定哪些已删除,哪些未删除,请查看生成的 x86 代码。 C# 编译器做的优化很少。 JIT 做了一些。
4.5 32 位和 64 位 JIT 是不同的代码库,具有不同的行为。一个新的 JIT (RyuJIT) 即将推出,在我的测试中通常表现更差,有时更好。
循环不能去掉,代码是没死,例如:
// 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
所有可能的结果,这就是为什么它保留循环
显然你的编译器不会忽略无用的代码,但会仔细分析它然后尝试删除它,如果它执行优化。
在你的例子中,第一个有趣的事情是变量 j 是否在循环之后使用。另一个有趣的事情是TestRunTime.Length。编译器会查看它并检查它是否总是 return 相同的结果,如果是,它是否有任何副作用,如果是,调用它一次是否与重复调用它具有相同的副作用。
如果 TestRunTime.Length 没有副作用并且不使用 j 则循环被删除。
否则,如果重复调用TestRunTime.Length比调用一次有更多的副作用,或者如果重复调用return不同的值,则必须执行循环。
否则,j = max (0, TestRunTime.Length)。
接下来,编译器可以判断是否需要赋值TestRunTime.Length。它可能会被仅确定 TestRunTime.Length 是什么的代码所取代。
当然你的编译器可能不会尝试任何花哨的优化,或者语言规则可能无法确定这些东西,你就被卡住了。
在大多数情况下,您不必担心主动删除无用代码。如果你 运行 遇到性能问题,并且你的分析器说一些无用的代码正在占用你的时钟周期,那么就去解决它。但是,如果代码真的什么都不做也没有副作用,那么它可能对 运行宁时间影响不大。
也就是说,大多数编译器不需要执行任何优化,因此依赖编译器优化并不总是最明智的选择。但在许多情况下,即使是无用的自旋循环也可以执行得非常快。循环一百万次的基本自旋锁会编译成类似 mov eax, 0 \ inc eax \ cmp eax, 1000000 \ jnz -8
的东西。即使我们不考虑 on-CPU 优化,每个循环也只有 3 个周期(在最近的 RISC 样式芯片上),因为没有内存访问,所以不会有任何缓存失效。在 1GHz CPU 上,只有 3,000,000/1,000,000,000 秒,即 3 毫秒。如果您尝试 运行每秒 60 次,那将是一个非常重要的打击,但在许多情况下,它甚至可能不会引人注意。
即使在 JIT 环境中,像我描述的那样的循环几乎肯定会针对 mov eax 1000000
进行窥孔优化。它可能会进一步优化,但在没有其他上下文的情况下,优化是合理的并且不会造成不良影响。
tl;dr:如果您的探查器说 dead/useless 代码正在使用您的 运行 时间资源的明显数量,请将其删除。不过,不要继续 寻找死代码;将其留给 rewrite/massive 重构。
好处:如果代码生成器知道 eax
除了循环条件之外不会被读取,并且想要保留自旋锁,它可以生成 mov eax, 1000000 \ dec eax \ jnz -3
并减少一个循环的循环惩罚。不过,大多数编译器会完全删除它。
我根据一些回答者关于使用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[].Lenght
:
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 |
---------------------------------------------
所以即使使用数组,它也根本没有得到优化。