C#:如何在使用多个函数时比内联代码执行得更快? 故事

C#:如何在使用多个函数时比内联代码执行得更快? 故事,c#,performance,delegates,C#,Performance,Delegates,所以,我想创建一个跨平台的小游戏,但后来我使用了不支持JIT的设备,比如IPhone、Windows mobile和Xbox One(游戏端,而不是应用端) 由于游戏必须从包含脚本的文本文件中生成一些“基本”代码,比如公式、赋值、调用函数、在每个对象的字典中修改/存储值(有点像混合交互式小说游戏),因此AOT编译实际上是不可能的 经过一番思考,我想出了一种解决方法,存储函数集合等等,以“模拟”普通代码。如果这种方式比编译的代码慢两倍,那么我会考虑删除不能运行JIT编译代码的设备。 我希望Visu

所以,我想创建一个跨平台的小游戏,但后来我使用了不支持JIT的设备,比如IPhone、Windows mobile和Xbox One(游戏端,而不是应用端)

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

经过一番思考,我想出了一种解决方法,存储函数集合等等,以“模拟”普通代码。如果这种方式比编译的代码慢两倍,那么我会考虑删除不能运行JIT编译代码的设备。 我希望VisualStudio中编译的代码速度最快,Linq.expression的速度最多慢10%

存储函数并为每一个和几乎所有的东西调用它们的黑客行为,我本以为会比编译代码慢很多,但是。。 太出乎我的意料了,速度更快了

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

测试 下面是我正在做的一个测试示例,“尝试”模拟代码的使用方式,其中有多个“脚本”,它们具有不同的函数和参数,在TestObject上运行。
代码中有趣的部分是:

  • 从PerfTest派生的类的构造函数
  • 执行(TestObject obj)它们覆盖的函数
这是用Visual Studio 2017编译的
.Net Framework 4.7.2
处于释放模式。
优化已打开。
平台目标=x86(尚未在ARM上测试)
用VisualStudio和单机版测试该程序,在性能上没有任何明显的差异

控制台测试程序 问题:
  • 为什么TestOther类的执行函数比两者都快 TestNormal和TestExpression

  • 我希望TestExpression更接近TestNormal,为什么离它这么远

您的“正常”实施

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"]);
}

它将按预期执行,比“其他”稍快一点。当有疑问时,将代码放入探查器。我查看了它,发现两个快速表达式和慢速编译表达式之间的主要区别在于字典查找性能

与其他版本相比,表达式版本在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
然后我检查了生成的汇编代码。它看起来几乎一模一样,无法解释表达式版本的巨大空白。 如果不同的东西被传递到字典[x]调用,我也会闯入Windbg,但一切看起来都很正常

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

您的基准测试代码在每次运行时都会调用一个随机测试类实例。我已将随机行走替换为始终选择第一个实例,而不是随机行走:

    for (int i = 0; i < data.Length; i++)
        //  sum += test[rnd.Next(test.Length)].Perform(data[i]);
        sum += test[0].Perform(data[i]);
您的代码的问题是/是,由于有许多间接操作,您确实将一个间接操作执行得太远,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的Core(TM)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 |

直接调用最快,其次是表达式,最后是委托方法。但这只是一个微观基准。正如您在开始时所发现的那样,您的实际性能数字可能会有所不同,甚至是违反直觉的。

Test expression会反复编译表达式。@我的名字是的,对于创建的TestExpression类型的每个处理程序,都会存储委托,然后在执行函数中重新使用。捕捉得不错,没有看到。随机部分在所有测试中都有相同的种子,所以它们在这个测试中同样更新。如何实际更新“TestObject”将来自游戏引擎的排队事件(物理、损坏、用户操作等),并带有id或对象引用。这将在下一帧或后台线程中更新。非常感谢您抽出时间详细介绍这些细节,非常感谢。此外,我的处理器与您的相同。:)选择此作为答案,因为您已经回答了这两个问题,并详细介绍了这两个问题。
Compiled Expression      740 milliseconds    sum = 4174863.85440933
"Normal"                 743 milliseconds    sum = 4174863.85430179
other                    714 milliseconds    sum = 4174863.85430179
Compiled Expression      1359 milliseconds   sum = 4174863.85440933
"Normal"                 775 milliseconds    sum = 4174863.85430179
other                    771 milliseconds    sum = 4174863.85430179
         Method |    N |     Mean |
--------------- |----- |---------:|
     TestNormal | 1000 | 3.175 us |
 TestExpression | 1000 | 3.480 us |
      TestOther | 1000 | 4.325 us |