我很好奇,如果一维数组比锯齿状数组快,我测量了以下代码块的性能:
测试1:锯齿状阵列
double[][][][] jagged = ArrayExtensions.Get4DMatrix<double>(100, 100, 50, 50, 0);
for (int iter = 0; iter < 5; iter++)
{
sw.Restart();
for (i = 0; i < 100; i++)
{
for (j = 0; j < 100; j++)
{
for (k = 0; k < 50; k++)
{
for (l = 0; l < 50; l++)
{
test = jagged[i][j][k][l];
jagged[i][j][k][l] = test;
}
}
}
}
Console.WriteLine("Jagged Arrays, Test {0}: {1} ms", iter, sw.ElapsedMilliseconds);
}
测试2:单维数组
double[] single = ArrayExtensions.Get1DArray<double>(25000000);
for (int iter = 0; iter < 5; iter++)
{
sw.Restart();
for (i = 0; i < 100; i++)
{
for (j = 0; j < 100; j++)
{
for (k = 0; k < 50; k++)
{
for (l = 0; l < 50; l++)
{
test = single[i * 100 + j * 100 + k * 50 + l];
single[i * 100 + j * 100 + k * 50 + l] = test;
}
}
}
}
Console.WriteLine("Single Arrays, Test {0}: {1} ms", iter, sw.ElapsedMilliseconds);
}
运行测试产生:
Jagged Arrays, Test 0: 1447 m
Jagged Arrays, Test 1: 1429 m
Jagged Arrays, Test 2: 1431 m
Jagged Arrays, Test 3: 1430 m
Jagged Arrays, Test 4: 1429 m
Single Arrays, Test 0: 386 ms
Single Arrays, Test 1: 387 ms
Single Arrays, Test 2: 386 ms
Single Arrays, Test 3: 387 ms
Single Arrays, Test 4: 387 ms
另外,我只运行了测试,只是分配给数组,然后只读取数组,结果具有相同的比率。
我期待一维数组比锯齿状数组更快,但当我看到最后一个块仅在第一个块的执行时间的27%执行时,我感到非常惊讶。
有人可以解释为什么会出现这种巨大差异吗?使用一维数组也有任何缺点(除了代码可读性之外,它显然变得更难,并且可能增加了出错的风险)?
代码是在非优化版本中执行的。在优化构建中,两次测试在每次迭代时都会在100毫秒内执行,但我认为这需要在循环内执行的代码中执行更多操作。尽管如此,1维阵列比锯齿状阵列快50%。
答案 0 :(得分:6)
test = single[i * 100 + j * 100 + k * 50 + l];
一位聪明的程序员曾经说过:“永远不要相信你没有自己伪造的基准”。可能是无意的,这是你的代码中一个非常讨厌的错误,让你比较苹果和橘子。乘数完全错误。 i
索引必须乘以100 * 50 * 50,j
索引乘以50 * 50。
副作用是你很多更有可能有效地使用CPU缓存,因为你解决了更少的内存。造成巨大差异,RAM非常慢。
答案 1 :(得分:0)
也许是因为“Jagged Arrays”是指针数组(到数组)...... 在您的示例中,您有4个级别的间接:
jagged[i][j][k][l]
答案 2 :(得分:0)
性能的一个主要因素是数据缓存未命中数。内存被分成称为缓存行的块,根据机器的不同,缓存行可能介于16-256字节左右。访问高速缓存行中的任何数据字节将花费与访问其中的所有内容相同的成本。最近访问的高速缓存行保存在CPU内核中的小型高速缓存中,并且可以非常快速地再次访问。最近未访问的行足以在第一级缓存中,将在二级缓存中查找,该缓存更大但访问速度不快。在那里找不到的行可以在第三级缓存中找到(理论上,第四,第五,第六等等,但我认为没有任何机器走得那么远)。一条指令要求在任何高速缓存中找不到的数据执行的时间可能要比使用一级高速缓存满足的数据长几十倍。
您的程序可能不是线性与锯齿状阵列相对性能的最佳度量标准,因为您使用的是完全顺序访问。这意味着大多数访问将由最快(1级)缓存处理。正如pspet所指出的,取消引用四个嵌套对象比计算单个嵌套对象需要更多的工作并使用它。如果一切都来自1级缓存,那么实际数据访问便宜的事实意味着这种额外的努力将占主导地位。
我建议您改变循环的顺序并监控性能。在“发布”模式下构建并在没有附加调试器的情况下运行以获得准确的计时结果。我猜想交换你的两个内部循环会使代码的两个版本大致相等(大多数数据请求可能不会被第一级缓存满足,但是对内层引用的请求将是),带来他们的相对时间越来越近。交换所有循环会削弱线性阵列版本的性能,但可能会导致嵌套的锯齿状阵列性能变得非常糟糕(您的外部数组可能会在第一级缓存中停留,但嵌套引用可能会不会,因为许多元素访问会导致两次或三次完全缓存未命中。)
对于占用超过85,000字节的数组,.NET存在性能损失,特别是如果它们是短暂的,因此在许多情况下,两级锯齿状阵列可能是最佳的。例如,如果数据项为64字节,则64位系统上的两个嵌套级别将允许一个具有1,024个项目的10,000个数组,每个项目没有任何项目超过85K。如果您需要超过10,000,000个项目,访问模式将决定您是否最好使用更大的阵列或第三级嵌套,但是上面的方法是最好的阵列大小。