我在调试应用程序时偶然发现了这种效果-请参阅下面的repro代码。
它给我以下结果:
Data init, count: 100,000 x 10,000, 4.6133365 secs
Perf test 0 (False): 5.8289565 secs
Perf test 0 (True): 5.8485172 secs
Perf test 1 (False): 32.3222312 secs
Perf test 1 (True): 217.0089923 secs
据我了解,数组存储操作通常不应该具有如此剧烈的性能影响(32秒与217秒)。我想知道是否有人知道这里发挥了什么作用?
增加了UPD额外测试; Perf 0显示预期结果,Perf 1-显示性能异常。
class Program
{
static void Main(string[] args)
{
var data = InitData();
TestPerf0(data, false);
TestPerf0(data, true);
TestPerf1(data, false);
TestPerf1(data, true);
if (Debugger.IsAttached)
Console.ReadKey();
}
private static string[] InitData()
{
var watch = Stopwatch.StartNew();
var data = new string[100_000];
var maxString = 10_000;
for (int i = 0; i < data.Length; i++)
{
data[i] = new string('-', maxString);
}
watch.Stop();
Console.WriteLine($"Data init, count: {data.Length:n0} x {maxString:n0}, {watch.Elapsed.TotalSeconds} secs");
return data;
}
private static void TestPerf1(string[] vals, bool testStore)
{
var watch = Stopwatch.StartNew();
var counters = new int[char.MaxValue];
int tmp = 0;
for (var j = 0; ; j++)
{
var allEmpty = true;
for (var i = 0; i < vals.Length; i++)
{
var val = vals[i];
if (j < val.Length)
{
allEmpty = false;
var ch = val[j];
var count = counters[ch];
tmp ^= count;
if (testStore)
counters[ch] = count + 1;
}
}
if (allEmpty)
break;
}
// prevent the compiler from optimizing away our computations
tmp.GetHashCode();
watch.Stop();
Console.WriteLine($"Perf test 1 ({testStore}): {watch.Elapsed.TotalSeconds} secs");
}
private static void TestPerf0(string[] vals, bool testStore)
{
var watch = Stopwatch.StartNew();
var counters = new int[65536];
int tmp = 0;
for (var i = 0; i < 1_000_000_000; i++)
{
var j = i % counters.Length;
var count = counters[j];
tmp ^= count;
if (testStore)
counters[j] = count + 1;
}
// prevent the compiler from optimizing away our computations
tmp.GetHashCode();
watch.Stop();
Console.WriteLine($"Perf test 0 ({testStore}): {watch.Elapsed.TotalSeconds} secs");
}
}
答案 0 :(得分:6)
在测试了一段时间的代码后,正如我在评论中已经说过的,我最好的猜测是,您当前的解决方案会遇到很多缓存丢失的情况。该行:
if (testStore)
counters[ch] = count + 1;
可能会强制编译器将新的缓存行完全加载到内存中并替换当前内容。在这种情况下,分支预测可能还会出现一些问题。这是高度依赖于硬件的,并且我还没有一个真正好的解决方案来以任何解释语言进行测试(在设置了硬件且众所周知的编译语言中也很难)。
经过反汇编后,您可以清楚地看到,您还引入了一堆新指令,这可能会进一步增加前面提到的问题。
总的来说,我建议您重新编写完整的算法,因为有更好的地方可以提高性能,而不是花点时间来做。这将是我建议的优化(这也会提高可读性):
i
和j
循环。这将完全删除allEmpty
变量。ch
将int
转换为var ch = (int) val[j];
-因为您始终将其用作索引。编辑:为什么我建议反转循环?通过以下代码重新排列:
foreach (var val in vals)
{
foreach (int ch in val)
{
var count = counters[ch];
tmp ^= count;
if (testStore)
{
counters[ch] = count + 1;
}
}
}
我来自这样的运行时:
到这样的运行时:
您仍然认为不值得尝试吗?我在这里节省了几个数量级,几乎消除了if
的影响(要清楚-设置中禁用了所有优化)。如果出于特殊原因不执行此操作,则应向我们详细说明将使用此代码的上下文。
EDIT2 :用于深入解答。对于出现此问题的最佳解释,是因为您交叉引用了缓存行。在各行中:
for (var i = 0; i < vals.Length; i++)
{
var val = vals[i];
您加载了非常庞大的数据集。这远远大于高速缓存行本身。因此,很可能需要在每次迭代中将内存中新鲜的迭代加载到新的缓存行(替换旧内容)中。如果我没记错的话,这也称为“缓存颠簸”。感谢@mjwills在他的评论中指出了这一点。
另一方面,在我建议的解决方案中,只要内部循环不超过其边界(如果使用此内存访问方向,发生的情况会少得多),高速缓存行的内容就可以保持活动状态。
这是最接近的解释,为什么我的代码运行速度如此之快,它还支持以下假设:您的代码存在严重的缓存问题。