C#:与内联代码相比,使用多个函数执行速度如何?

标签 c# performance delegates

故事

所以,我想创建一个跨平台的小游戏,但后来我在不支持 JIT 的设备上结束了,例如 iPhone、Windows mobile 和 Xbox One(游戏端,而不是应用程序端)。

由于游戏必须从包含脚本的文本文件中生成一些“基本”代码,例如公式、赋值、调用函数、修改/存储每个对象字典中的值(有点像混合互动小说游戏) ,用 AOT 编译是不可能的。

经过一番思考,我想出了一个解决方法,存储函数的集合,而不是“模拟”普通代码。如果这种方式比编译代码慢很多,那么我会考虑放弃无法运行 JIT 编译代码的设备。

我原以为 visual studio 中的编译代码速度最快,而 Linq.Expressions 最多慢 10%。

存储函数并为每个函数和几乎所有函数调用它们的技巧,我预计会比编译代码慢很多,但是.. 太让我吃惊了,它更快了???

注意:
这个项目主要是关于我空闲时间的学习和个人兴趣。
最终产品只是一种奖励,能够出售或使其开源。

测试

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

  • 派生自 PerfTest 的类的构造函数。
  • 它们覆盖的 Perform(TestObject obj) 函数。

这是用 Visual Studio 2017 编译的
.Net Framework 4.7.2
处于 Release模式。
已开启优化。
平台目标 = x86(尚未在 ARM 上测试)
使用 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,为什么相差这么远?

最佳答案

如有疑问,请将代码放入分析器中。我看过它,发现这两个快速的和慢速编译的 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 查找,但这对我们的因素二没有任何作用),但表达式版本需要两倍的 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 是棘手的野兽。

我确实在 Core(TM) i7-4770K CPU @ 3.50GHz 上运行了我的代码。

众所周知,字典对于缓存预测器来说非常糟糕,因为它们往往会在内存中疯狂地跳来跳去,而找不到任何模式。许多字典调用似乎已经使预测器相当困惑,并且所用测试实例的额外随机性和编译表达式的更复杂调度对于 CPU 来说太多了,无法预测内存访问模式并将其部分预取到 L1/2 缓存。实际上,您不是在测试调用性能,而是在测试 CPU 缓存策略的性能。

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

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

直接调用最快,其次是表达式,最后是委托(delegate)方法。但那是一个微观基准。您的实际性能数据可能与您在开始时发现的不同,甚至有悖于直觉。

关于C#:与内联代码相比,使用多个函数执行速度如何?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53795804/

相关文章:

c# - 除以零没有错误?

c# - 具有批量生产者的生产者/消费者模式

c# - 使用 IEnumerable<T> 作为委托(delegate)返回类型

python - 提高在二维 numpy 数组中查找最小元素的速度,该数组有许多条目设置为 np.inf

sql - INNER JOIN 性能是否取决于表的顺序?

qt - QML:组件与项作为容器

c# - 通用委托(delegate) C#

c# - 如何使 'click event' 成为用户控件在 visual studio 中的默认操作

c# - 将 LINQ Except() 与两个不同类型的集合一起使用

java - 一次性数据转换的JDBC批量更新