C#:与内联代码相比,它在多种功能上的执行速度如何?

时间:2018-12-15 17:56:03

标签: c# performance delegates

故事

因此,我想为跨平台创建一个小型游戏,但后来我遇到了不支持JIT的设备,例如IPhone,Windows Mobile和Xbox One(游戏方面,而不是应用程序方面)。 / p>

由于该游戏必须从其中包含脚本的文本文件中生成一些“基本”代码,例如公式,赋值,调用函数,在每个对象的字典中修改/存储值(类似于混合交互式小说游戏) ,实际上是不可能通过AOT编译完成的。

经过一番思考,我想出了一种解决方法,可以存储功能的集合,而不是存储功能的集合,以“模仿”普通代码。如果这种方式的速度比编译代码慢两倍,那么我将考虑删除无法运行JIT编译代码的设备。

我希望Visual Studio中的编译代码能被禁食,而Linq.Expression的速度最多可慢10%。

存储功能并为几乎所有功能调用它们的方法,我希望比编译的代码慢很多,但是。 让我感到惊讶,速度更快?

注意:
这个项目主要是关于我的业余时间的学习和个人兴趣。
最终产品只是奖金,能够销售或使其开源。

测试

这里是我正在做的一个测试示例,并“尝试”对如何使用代码进行建模,其中有多个具有不同功能和参数的“脚本”在TestObject上运行。
代码中有趣的部分是:

  • 从PerfTest派生的类的构造函数。
  • 它们重写的Perform(TestObject obj)函数。

这是使用Visual Studio 2017编译的
.Net Framework 4.7.2
在释放模式下。
优化已启用。
平台目标= x86(尚未在ARM上进行测试)
在独立的Visual Studio上对Visual Studio进行了测试,性能没有明显变化。

控制台测试程序

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            new PerformanceTest();

            Console.WriteLine();
            Console.WriteLine("Done, press enter to exit");
            Console.ReadLine();
        }
    }
    class TestObject
    {
        public Dictionary<string, float> data = new Dictionary<string, float>();
        public TestObject(Random rnd)
        {
            data.Add("A", (float)rnd.NextDouble());
            data.Add("B", (float)rnd.NextDouble());
            data.Add("C", (float)rnd.NextDouble());
            data.Add("D", (float)rnd.NextDouble() + 1.0f);
            data.Add("E", (float)rnd.NextDouble());
            data.Add("F", (float)rnd.NextDouble() + 1.0f);
        }
    }
    class PerformanceTest
    {
        Stopwatch timer = new Stopwatch();
        public PerformanceTest()
        {
            var rnd = new Random(1);
            int testSize = 5000000;
            int testTimes = 5;
            Console.WriteLine($"Creating {testSize} objects to test performance with");

            timer.Start();
            var data = new TestObject[testSize];
            for (int i = 0; i < data.Length; i++)
                data[i] = new TestObject(rnd);
            Console.WriteLine($"Created objects in {timer.ElapsedMilliseconds} milliseconds");

            int handlers = 1000;

            Console.WriteLine($"Creating {handlers} handlers per type");
            var tests = new PerfTest[3][];
            tests[0] = new PerfTest[handlers];
            tests[1] = new PerfTest[handlers];
            tests[2] = new PerfTest[handlers];

            for (int i = 0; i < tests[0].Length; i++)
                tests[0][i] = new TestNormal();
            for (int i = 0; i < tests[1].Length; i++)
                tests[1][i] = new TestExpression();
            for (int i = 0; i < tests[2].Length; i++)
                tests[2][i] = new TestOther();

            Console.WriteLine($"Handlers created");
            Console.WriteLine($"Warming up all handlers");

            for (int t = 0; t < tests.Length; t++)
                for (int i = 0; i < tests[t].Length; i++)
                    tests[t][i].Perform(data[0]);

            Console.WriteLine($"Testing data {testTimes} times with handlers of each type");
            for (int i = 0; i < testTimes; i++)
            {
                Console.WriteLine();
                for (int t = 0; t < tests.Length; t++)
                    Loop(tests[t], data);
            }

            timer.Stop();
        }

        void Loop(PerfTest[] test, TestObject[] data)
        {
            var rnd = new Random(1);
            var start = timer.ElapsedMilliseconds;
            double sum = 0;

            for (int i = 0; i < data.Length; i++)
                sum += test[rnd.Next(test.Length)].Perform(data[i]);

            var stop = timer.ElapsedMilliseconds;
            var elapsed = stop - start;

            Console.WriteLine($"{test[0].Name}".PadRight(25) + $"{elapsed} milliseconds".PadRight(20) + $"sum = { sum}");
        }
    }
    abstract class PerfTest
    {
        public string Name;
        public abstract float Perform(TestObject obj);
    }
    class TestNormal : PerfTest
    {
        public TestNormal()
        {
            Name = "\"Normal\"";
        }
        public override float Perform(TestObject obj) => obj.data["A"] * obj.data["B"] + obj.data["C"] / obj.data["D"] + obj.data["E"] / (obj.data["E"] + obj.data["F"]);
    }
    class TestExpression : PerfTest
    {
        Func<TestObject, float> compiledExpression;
        public TestExpression()
        {
            Name = "Compiled Expression";
            var par = Expression.Parameter(typeof(TestObject));
            var body = Expression.Add(Expression.Multiply(indexer(par, "A"), indexer(par, "B")), Expression.Add(Expression.Divide(indexer(par, "C"), indexer(par, "D")), Expression.Divide(indexer(par, "E"), Expression.Add(indexer(par, "E"), indexer(par, "F")))));

            var lambda = Expression.Lambda<Func<TestObject, float>>(body, par);
            compiledExpression = lambda.Compile();
        }
        static Expression indexer(Expression parameter, string index)
        {
            var property = Expression.Field(parameter, typeof(TestObject).GetField("data"));
            return Expression.MakeIndex(property, typeof(Dictionary<string, float>).GetProperty("Item"), new[] { Expression.Constant(index) });
        }

        public override float Perform(TestObject obj) => compiledExpression(obj);
    }
    class TestOther : PerfTest
    {
        Func<TestObject, float>[] parameters;
        Func<float, float, float, float, float, float, float> func;
        public TestOther()
        {
            Name = "other";
            Func<float, float, float, float, float, float, float> func = (a, b, c, d, e, f) => a * b + c / d + e / (e + f);
            this.func = func; // this delegate will come from a collection of functions, depending on type

            parameters = new Func<TestObject, float>[]
            {
                (o) => o.data["A"],
                (o) => o.data["B"],
                (o) => o.data["C"],
                (o) => o.data["D"],
                (o) => o.data["E"],
                (o) => o.data["F"],
            };
        }
        float call(TestObject obj, Func<float, float, float, float, float, float, float> myfunc, Func<TestObject, float>[] parameters)
        {
            return myfunc(parameters[0](obj), parameters[1](obj), parameters[2](obj), parameters[3](obj), parameters[4](obj), parameters[5](obj));
        }
        public override float Perform(TestObject obj) => call(obj, func, parameters);
    }
}

此控制台测试的输出结果:

Creating 5000000 objects to test performance with
Created objects in 7489 milliseconds
Creating 1000 handlers per type
Handlers created
Warming up all handlers
Testing data 5 times with handlers of each type

"Normal"                 811 milliseconds    sum = 4174863.85436047
Compiled Expression      1371 milliseconds   sum = 4174863.85436047
other                    746 milliseconds    sum = 4174863.85436047

"Normal"                 812 milliseconds    sum = 4174863.85436047
Compiled Expression      1379 milliseconds   sum = 4174863.85436047
other                    747 milliseconds    sum = 4174863.85436047

"Normal"                 812 milliseconds    sum = 4174863.85436047
Compiled Expression      1373 milliseconds   sum = 4174863.85436047
other                    747 milliseconds    sum = 4174863.85436047

"Normal"                 812 milliseconds    sum = 4174863.85436047
Compiled Expression      1373 milliseconds   sum = 4174863.85436047
other                    747 milliseconds    sum = 4174863.85436047

"Normal"                 812 milliseconds    sum = 4174863.85436047
Compiled Expression      1375 milliseconds   sum = 4174863.85436047
other                    746 milliseconds    sum = 4174863.85436047

Done, press enter to exit

问题

  • 为什么类TestOther的Perform函数比两者都快 TestNormal和TestExpression?

  • 我希望TestExpression距离TestNormal更近,为什么这么远呢?

2 个答案:

答案 0 :(得分:2)

如有疑问,请将代码放入探查器。我查看了它,发现两个快速表达式和慢速编译的Expression之间的主要区别是字典查找性能。

Expression版本在Dictionary FindEntry中所需的CPU数量是其他版本的两倍以上。

Stack                                                                           Weight (in view) (ms)
GameTest.exe!Test.PerformanceTest::Loop                                         15,243.896600
  |- Anonymously Hosted DynamicMethods Assembly!dynamicClass::lambda_method      6,038.952700
  |- GameTest.exe!Test.TestNormal::Perform                                       3,724.253300
  |- GameTest.exe!Test.TestOther::call                                           3,493.239800

然后,我确实检查了生成的汇编代码。它看起来几乎完全相同,无法解释表达式版本松散的巨大余地。 如果将不同的内容传递给Dictionary [x]调用,我也确实闯入了Windbg,但看上去确实很正常。

总而言之,所有版本的工作量基本上相同(减去字典版本的双E查找,但对我们的因子2没有作用),但是Expression版本需要两倍的CPU。这真的是一个谜。

您的基准代码调用每次都运行一个随机测试类实例。我已经通过始终采用第一个实例而不是随机实例代替了该随机游走:

    for (int i = 0; i < data.Length; i++)
        //  sum += test[rnd.Next(test.Length)].Perform(data[i]);
        sum += test[0].Perform(data[i]);

现在我得到了更好的值:

Compiled Expression      740 milliseconds    sum = 4174863.85440933
"Normal"                 743 milliseconds    sum = 4174863.85430179
other                    714 milliseconds    sum = 4174863.85430179

您的代码的问题是/由于许多间接,您确实使一个间接距离太远,并且CPU的分支预测器不再能够预测涉及两跳的编译表达式的下一个调用目标。当我使用随机游走时,我会得到“糟糕”的表现:

Compiled Expression      1359 milliseconds   sum = 4174863.85440933
"Normal"                 775 milliseconds    sum = 4174863.85430179
other                    771 milliseconds    sum = 4174863.85430179

观察到的不良行为在很大程度上取决于CPU,并且与CPU代码和数据缓存大小有关。我手头没有VTune来备份数字,但这再次表明当今的CPU是棘手的野兽。

我确实在3.50GHz的CoreTM i7-4770K CPU上运行了代码。

众所周知,字典对于缓存预测器非常不利,因为它们往往会在找不到模式的内存中随意跳转。许多字典调用似乎已经使预测变量产生了很大的混乱,并且所用测试实例的额外随机性和编译表达式的更复杂的派发对于CPU来说无法预测内存访问模式并将其部分预取到L1 / 2个缓存。实际上,您并不是在测试呼叫性能,而是测试CPU缓存策略的性能。

您应该重构测试代码以使用更简单的调用模式,并可能使用Benchmark.NET将这些因素分解出来。这样得出的结果符合您的期望:

         Method |    N |     Mean |
--------------- |----- |---------:|
     TestNormal | 1000 | 3.175 us |
 TestExpression | 1000 | 3.480 us |
      TestOther | 1000 | 4.325 us |

直接调用最快,接下来是表达式,最后是委托方法。但这只是一个微观基准。您的实际表现数字可能与您一开始发现的数字有所不同,甚至与直觉相反。

答案 1 :(得分:1)

您的“正常”实施方式

public override float Perform(TestObject obj)
{
    return obj.data["A"] * obj.data["B"] 
         + obj.data["C"] / obj.data["D"]
         + obj.data["E"] / (obj.data["E"] + obj.data["F"]);
}

效率低下。它两次调用obj.data["E"],而“其他”实现仅调用一次。您稍微修改一下代码

public override float Perform(TestObject obj)
{
    var e = obj.data["E"];
    return obj.data["A"] * obj.data["B"] 
         + obj.data["C"] / obj.data["D"] 
         + e / (e + obj.data["F"]);
}

它会按预期执行,比“其他”要快。