阵列写入的性能影响远大于预期

时间:2018-11-09 04:24:45

标签: c# .net performance

我在调试应用程序时偶然发现了这种效果-请参阅下面的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");
    }
}

1 个答案:

答案 0 :(得分:6)

在测试了一段时间的代码后,正如我在评论中已经说过的,我最好的猜测是,您当前的解决方案会遇到很多缓存丢失的情况。该行:

if (testStore)
    counters[ch] = count + 1;

可能会强制编译器将新的缓存行完全加载到内存中并替换当前内容。在这种情况下,分支预测可能还会出现一些问题。这是高度依赖于硬件的,并且我还没有一个真正好的解决方案来以任何解释语言进行测试(在设置了硬件且众所周知的编译语言中也很难)。

经过反汇编后,您可以清楚地看到,您还引入了一堆新指令,这可能会进一步增加前面提到的问题。

enter image description here

总的来说,我建议您重新编写完整的算法,因为有更好的地方可以提高性能,而不是花点时间来做。这将是我建议的优化(这也会提高可读性):

  1. 反转ij循环。这将完全删除allEmpty变量。
  2. 使用chint转换为var ch = (int) val[j];-因为您始终将其用作索引。
  3. 考虑一下为什么这可能是个问题。您引入了新的说明,任何说明都是有代价的。如果这确实是代码的主要“热点”,那么您可以开始考虑更好的解决方案(请记住:“过早的优化是万恶之源”。)
  4. 这是顾名思义的“测试设置”,这是否很重要?只需将其删除即可。

编辑:为什么我建议反转循环?通过以下代码重新排列:

foreach (var val in vals)
{
    foreach (int ch in val)
    {
        var count = counters[ch];
        tmp ^= count;
        if (testStore)
        {
            counters[ch] = count + 1;
        }
    }
}

我来自这样的运行时:

enter image description here

到这样的运行时:

enter image description here

您仍然认为不值得尝试吗?我在这里节省了几个数量级,几乎消除了if的影响(要清楚-设置中禁用了所有优化)。如果出于特殊原因不执行此操作,则应向我们详细说明将使用此代码的上下文。


EDIT2 :用于深入解答。对于出现此问题的最佳解释,是因为您交叉引用了缓存行。在各行中:

for (var i = 0; i < vals.Length; i++)
{
    var val = vals[i];

您加载了非常庞大的数据集。这远远大于高速缓存行本身。因此,很可能需要在每次迭代中将内存中新鲜的迭代加载到新的缓存行(替换旧内容)中。如果我没记错的话,这也称为“缓存颠簸”。感谢@mjwills在他的评论中指出了这一点。

另一方面,在我建议的解决方案中,只要内部循环不超过其边界(如果使用此内存访问方向,发生的情况会少得多),高速缓存行的内容就可以保持活动状态。

这是最接近的解释,为什么我的代码运行速度如此之快,它还支持以下假设:您的代码存在严重的缓存问题。