C# 不同优化的无法解释的时间
我正在编写一些代码,这些代码必须根据设置对大型数据集应用不同的算法。数据集很大,现实世界的时间安排表明,如果可能,我们需要对此进行优化 所选算法必须在来自大型阵列的多个子集数据上运行。因此,我决定尝试几种不同的方法:C# 不同优化的无法解释的时间,c#,optimization,C#,Optimization,我正在编写一些代码,这些代码必须根据设置对大型数据集应用不同的算法。数据集很大,现实世界的时间安排表明,如果可能,我们需要对此进行优化 所选算法必须在来自大型阵列的多个子集数据上运行。因此,我决定尝试几种不同的方法: 初始化Func委托以引用所需的算法。从主循环中调用此委托 在数据上循环,并从主循环中调用适当的算法 调用一个单独的方法,该方法为每个算法实现主循环本身 在我的测试中,我让每种方法调用相同的底层方法,calculate()。(当然,实际代码会为每个算法调用不同的方法,但这里我测试的是
Func
委托以引用所需的算法。从主循环中调用此委托calculate()
。(当然,实际代码会为每个算法调用不同的方法,但这里我测试的是调用算法的最快方法,而不是算法本身。)
每个测试在循环ITERS
次中调用所需的算法
在这个测试代码中,datareducationalgorithm
只是定义各种算法的枚举。除了模拟真实代码中发生的情况之外,它并没有真正被使用
下面是我对方法(1)的测试实现。非常简单:将Func a
分配给要调用的算法,然后从循环中调用它:
private static void test1(int[] data, DataReductionAlgorithm algorithm)
{
Func<int[], int, int, int> a;
switch (algorithm)
{
case DataReductionAlgorithm.Max:
a = calculate;
break;
case DataReductionAlgorithm.Mean:
a = calculate;
break;
default:
a = calculate;
break;
}
for (int i = 0; i < ITERS; ++i)
a(data, 0, data.Length);
}
下面是测试方法的代码(3)。如果移动If
测试,以在循环内选择算法。我原以为这种方法会慢一些(2),因为if
测试将执行ITERS
次,而不是一次:
private static void test3(int[] data, DataReductionAlgorithm algorithm)
{
for (int i = 0; i < ITERS; ++i)
{
switch (algorithm)
{
case DataReductionAlgorithm.Max:
calculate(data, 0, data.Length);
break;
case DataReductionAlgorithm.Mean:
calculate(data, 0, data.Length);
break;
default:
calculate(data, 0, data.Length);
break;
}
}
}
以下是整个程序,以防有人想亲自尝试:
using System;
using System.Diagnostics;
using System.Linq;
namespace Demo
{
public enum DataReductionAlgorithm
{
Single,
Max,
Mean
}
internal class Program
{
private const int ITERS = 100000;
private void run()
{
int[] data = Enumerable.Range(0, 10000).ToArray();
Stopwatch sw = new Stopwatch();
for (int trial = 0; trial < 4; ++trial)
{
sw.Restart();
test1(data, DataReductionAlgorithm.Mean);
Console.WriteLine("test1: " + sw.Elapsed);
sw.Restart();
test2(data, DataReductionAlgorithm.Mean);
Console.WriteLine("test2: " + sw.Elapsed);
sw.Restart();
test3(data, DataReductionAlgorithm.Mean);
Console.WriteLine("test3: " + sw.Elapsed);
sw.Restart();
test4(data, DataReductionAlgorithm.Mean);
Console.WriteLine("test4: " + sw.Elapsed);
Console.WriteLine();
}
}
private static void test1(int[] data, DataReductionAlgorithm algorithm)
{
Func<int[], int, int, int> a;
switch (algorithm)
{
case DataReductionAlgorithm.Max:
a = calculate;
break;
case DataReductionAlgorithm.Mean:
a = calculate;
break;
default:
a = calculate;
break;
}
for (int i = 0; i < ITERS; ++i)
a(data, 0, data.Length);
}
private static void test2(int[] data, DataReductionAlgorithm algorithm)
{
switch (algorithm)
{
case DataReductionAlgorithm.Max:
for (int i = 0; i < ITERS; ++i)
calculate(data, 0, data.Length);
break;
case DataReductionAlgorithm.Mean:
for (int i = 0; i < ITERS; ++i)
calculate(data, 0, data.Length);
break;
default:
for (int i = 0; i < ITERS; ++i)
calculate(data, 0, data.Length);
break;
}
}
private static void test3(int[] data, DataReductionAlgorithm algorithm)
{
for (int i = 0; i < ITERS; ++i)
{
switch (algorithm)
{
case DataReductionAlgorithm.Max:
calculate(data, 0, data.Length);
break;
case DataReductionAlgorithm.Mean:
calculate(data, 0, data.Length);
break;
default:
calculate(data, 0, data.Length);
break;
}
}
}
private static void test4(int[] data, DataReductionAlgorithm algorithm)
{
switch (algorithm)
{
case DataReductionAlgorithm.Max:
iterate(ITERS, data);
break;
case DataReductionAlgorithm.Mean:
iterate(ITERS, data);
break;
default:
iterate(ITERS, data);
break;
}
}
private static void iterate(int n, int[] data)
{
for (int i = 0; i < n; ++i)
calculate(data, 0, data.Length);
}
private static int calculate(int[] data, int i1, int i2)
{
// Just a dummy implementation.
// Using the same algorithm for each approach to avoid differences in timings.
int result = 0;
for (int i = i1; i < i2; ++i)
result += data[i];
return result;
}
private static void Main()
{
new Program().run();
}
}
}
这正是我所期望的,只是test1()比我想象的要快(可能表明调用委托是高度优化的)
以下是x64的结果:
test1: 00:00:00.8769743
test2: 00:00:00.8750667
test3: 00:00:00.5839475
test4: 00:00:00.5853400
哇
test1()
和test2()
发生了什么?我无法解释。test2()
怎么会比test3()
慢这么多
为什么test4()
的速度与test2()的速度不一样呢
为什么x86和x64之间存在巨大差异
有人能解释一下吗?速度上的差异并不是微不足道的——它可能会在10秒和15秒之间产生差异
附录
我已经接受了下面的答案
但是,为了说明下面的@ UR所提到的JIT优化的脆弱性,请考虑下面的代码:
using System;
using System.Diagnostics;
namespace Demo
{
internal class Program
{
private const int ITERS = 10000;
private void run()
{
Stopwatch sw = new Stopwatch();
int[] data = new int[10000];
for (int trial = 0; trial < 4; ++trial)
{
sw.Restart();
test1(data, 0);
var elapsed1 = sw.Elapsed;
sw.Restart();
test2(data, 0);
var elapsed2 = sw.Elapsed;
Console.WriteLine("Ratio = " + elapsed1.TotalMilliseconds / elapsed2.TotalMilliseconds);
}
Console.ReadLine();
}
private static void test1(int[] data, int x)
{
switch (x)
{
case 0:
{
for (int i = 0; i < ITERS; ++i)
dummy(data);
break;
}
}
}
private static void test2(int[] data, int x)
{
switch (x)
{
case 0:
{
loop(data);
break;
}
}
}
private static int dummy(int[] data)
{
int max = 0;
// Also try with "int i = 1" in the loop below.
for (int i = 0; i < data.Length; ++i)
if (data[i] > max)
max = data[i];
return max;
}
private static void loop(int[] data)
{
for (int i = 0; i < ITERS; ++i)
dummy(data);
}
private static void Main()
{
new Program().run();
}
}
}
只需将其更改为i=1
,即可得到以下结果:
Ratio = 1.16920209593233
Ratio = 0.990370350435142
Ratio = 0.991150637472754
Ratio = 0.999941245001628
有趣!:) 我可以在x64、.NET 4.5版本上重现该问题,无需调试器
我已经查看了为test2
和test3
生成的x64。热内循环占用99%的时间。只有这个循环才重要
对于test3
,calculate
是内联的,循环边界等于数组边界。这允许JIT消除范围检查。在test2
中,无法消除范围检查,因为循环边界是动态的。它们由inti1、inti2
给出,它们在静态上不是有效的数组边界。只有内联可以在当前JIT中提供该信息。内联将这些值替换为0,data.Length
事实并非如此。这个NET JIT在这方面并不复杂
test3
带内联:
test2
计算未内联:
两个分支而不是一个。一个是循环测试,一个是范围检查
我不知道为什么这里的JIT内联方式不同。内联是由启发式驱动的。我在我的PC上测试过它,在x86和x64上都得到了类似的结果。你用笔记本电脑做测试吗?@AlexH不,这是台式电脑。你肯定是在运行发布版本而不是在调试器下吗?因为calculate
是一个非常长时间运行的函数,它应该完全控制基准测试的运行时间。由于testX函数而产生的所有差异都应该消失在噪声中。@usr但这些差异显然没有消失,如结果所示。特别是,将test2()与test4()进行比较。他们真的应该是几乎相同的时间。(在任何情况下,这如何解释x86和x64之间的差异呢?@AlexH我也在其他一些系统上尝试过这个方法,得到了相同的结果。很有趣。我想这其中的寓意是“不要在switch语句中放入循环-调用执行循环的方法”。通过这样做并将test2()
转换为test4()
,我们可以获得50%的速度提升!我猜开关
构造可能会引入太多的复杂性,抖动无法处理。通过将循环放入一个单独的循环中,我们将其简化到足以使抖动得到优化。如果你问我,开关是不相关的。谁知道什么样的小干扰可以抑制内联呢。你不能可靠地保护自己不受伤害。编译器启发法非常脆弱。我会尝试将数组边界计算移到循环所在的函数中。使用常量零和data.Length
作为循环边界以确保安全。或者使用不安全的代码,尽管我也看到过这样的情况,即JIT无法生成好的代码。
test1: 00:00:00.8769743
test2: 00:00:00.8750667
test3: 00:00:00.5839475
test4: 00:00:00.5853400
using System;
using System.Diagnostics;
namespace Demo
{
internal class Program
{
private const int ITERS = 10000;
private void run()
{
Stopwatch sw = new Stopwatch();
int[] data = new int[10000];
for (int trial = 0; trial < 4; ++trial)
{
sw.Restart();
test1(data, 0);
var elapsed1 = sw.Elapsed;
sw.Restart();
test2(data, 0);
var elapsed2 = sw.Elapsed;
Console.WriteLine("Ratio = " + elapsed1.TotalMilliseconds / elapsed2.TotalMilliseconds);
}
Console.ReadLine();
}
private static void test1(int[] data, int x)
{
switch (x)
{
case 0:
{
for (int i = 0; i < ITERS; ++i)
dummy(data);
break;
}
}
}
private static void test2(int[] data, int x)
{
switch (x)
{
case 0:
{
loop(data);
break;
}
}
}
private static int dummy(int[] data)
{
int max = 0;
// Also try with "int i = 1" in the loop below.
for (int i = 0; i < data.Length; ++i)
if (data[i] > max)
max = data[i];
return max;
}
private static void loop(int[] data)
{
for (int i = 0; i < ITERS; ++i)
dummy(data);
}
private static void Main()
{
new Program().run();
}
}
}
Ratio = 1.52235829774506
Ratio = 1.50636405328076
Ratio = 1.52291602053827
Ratio = 1.52803278744701
Ratio = 1.16920209593233
Ratio = 0.990370350435142
Ratio = 0.991150637472754
Ratio = 0.999941245001628