【问题标题】:C#'s general performance in Unity seems orders of magnitude slower than JVM-based languageC# 在 Unity 中的一般性能似乎比基于 JVM 的语言慢几个数量级
【发布时间】:2021-05-25 17:58:22
【问题描述】:

在 Unity 中,我注意到与 Kotlin 中的类似实现相比,我在某些代码逻辑中的性能低于标准。分析后,我怀疑语言/运行时本身可能会以某种方式变慢。因此,我在 Kotlin 和 C# 中都做了一个非常简短的基准测试来衡量基本操作的性能:

kotlin部分如下。请注意,Matrix4Vector3 是 libGDX(Java/Kotlin 的游戏库)类,它们只不过是数据的容器。 mul 函数将矩阵与向量相乘,并将结果原位存储回向量中。

fun benchmark(a: Matrix4, b: List<Vector3>) {
    var i = 0;
    while (i < 100000) {
        b[i].mul(a);
        ++i;
    }
}
 
var a = Matrix4(floatArrayOf(1f, 2f, 3f, 4f, 3f, 2f, 1f, 2f, 3f, 4f, 3f, 2f, 1f, 2f, 3f, 4f))
var b = List<Vector3>();
for (i in 0..100000) {
    b.add(Vector3(3f, 2f, 1f));
}
// warmup JIT
for (i in 0..9) {
    benchmark(a, b)
}
var t: Double = 0.0;
for (i in 0..9) {
    t += measureNanoTime {
        benchmark(a, b)
    }.toDouble()
}
println(t / 10.0 / 1000000.0) // milliseconds

Unity C#部分如下。请注意,M4V3 是为匹配 libGDX 所创建的辅助类。

private void Benchmark(M4 a, List<V3> b)
{
    var i = 0;
    while (i < 100000)
    {
        b[i].mul(a);
        ++i;
    }
}
 
var a = new M4(1f, 2f, 3f, 4f, 3f, 2f, 1f, 2f, 3f, 4f, 3f, 2f, 1f, 2f, 3f, 4f);
var b = new List<V3>();
for (int i = 0; i < 100000; ++i)
{
    b.Add(new V3(3, 2, 1));
}
// warmup JIT
for (int i = 0; i < 10; ++i)
{
    Benchmark(a, b);
}
var t = 0.0;
for (int i = 0; i < 10; ++i)
{
    var s = (double) nanoTime();
    Benchmark(a, b);
    var e = (double) nanoTime();
    t += e - s;
}
Debug.Log(t / 10.0 / 1000000.0); // milliseconds

mul 的实现与 libGDX 的精确实现 (https://github.com/libgdx/libgdx/bl...x/src/com/badlogic/gdx/math/Vector3.java#L353) 相匹配。

该设备是 2015 年中的 MacBook Pro。 Unity 版本是 2020.3.0f1,使用 Mono 后端构建到 OSX 独立版本,而不是开发版本。

结果如下:

  • Kotlin:0.3658762ms
  • Unity C#:1.74067 毫秒(几乎慢了 4 倍)。如果我将 M4V3 更改为结构而不是类,它会变得更慢:2.51 毫秒(几乎慢了 6 倍)。

造成如此显着差异的原因是什么?

【问题讨论】:

  • 嗯,存在Vector3 之类的东西是有原因的,并与magic 相关联。运行时不是特别适合在使用通用代码时优化通用数字运算(“透视”原语)。 Java VM 在优化方面也比 .NET VM 有很大的领先优势,尽管这个部门肯定有很多工作要做。
  • 这两个实现不一样。并且使用双精度数来计算时间是不安全的,并且会受到缩放和舍入问题的影响。请改用Stopwatch
  • SIMD CPU 指令和使用它们的类型(如 Vector3)的重要性怎么强调都不为过。自 2000 年代以来的所有 CPU 都可以一次处理多个浮点数/整数/双精度数。他们还优化了 3D 转换,由 4x4 矩阵乘法执行。 Vector3 和 Matrix4x4 不仅仅是为了方便。他们的很多操作实际上都是使用 SIMD 指令进行的
  • @PanagiotisKanavos 我使用stackoverflow.com/a/44136515/3308553 找到nanoTime,它在下面使用Stopwatch。此外,从 libGDX 的源代码 (github.com/libgdx/libgdx/bl...x/src/com/badlogic/gdx/math/…) 来看,他们的类似乎没有为我在基准测试中调用的特定函数明确使用 SIMD。
  • 如果你需要做很多向量和其他数学运算,不如使用 Unity 的 JobSystemBurst Compiler

标签: java c# performance unity3d kotlin


【解决方案1】:

如果以不同的方式完成,C# 的性能可能会更好。

首先,Unity Engine 在使用MonoBehaviour 时,MonoBehaviour 类下的大部分代码都是Single Threaded,这意味着完成代码可能需要很长的时间,尤其是涉及更多数学时。

然后,Unity 不仅编译和运行代码,除了运行代码它还进行渲染,在编辑器中进行一些计算以获得游戏的性能, 许多进程会在后台运行,只是为了运行 Unity,一些 RAM 和大部分 CPU 功率将用于运行 Unity Engine 及其编辑器,因此代码无法从硬件中获得最大功率。

所以,要充分利用 Unity 和 C#,

试试 Unity 的 DOTS(面向数据的技术堆栈),它改变了游戏规则。 Burst 通过编译称为高性能 C# (HPC#) 的 C# 语言子集来工作,通过部署构建在 LLVM 编译器框架之上的高级优化来有效利用设备的功能。 Burst 非常适合利用应用程序中的隐藏并行性。

DOTS 的主要功能是在多线程中运行游戏,并使用JobSystemComponentSystem 等来利用硬件。

大多数类在 DOTS 中得到了它的新名称,与 Vector3 不同,Burst Compiler Supported,burst 具有 float3, float4,这极大地提高了性能。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-11-30
    • 2012-04-23
    • 2011-04-09
    • 1970-01-01
    • 1970-01-01
    • 2016-06-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多