【问题标题】:Optimizing this enormous for loop优化这个巨大的 for 循环
【发布时间】:2021-05-24 14:23:42
【问题描述】:

所以我有这个 for 循环:

for (int i = 0; i < meshes.Count; i++)
        {
            for (int j = 0; j < meshes.Count; j++)
            {
                for (int m = 0; m < meshes[i].vertices.Length; m++)
                {
                    for (int n = 0; n < meshes[i].vertices.Length; n++)
                    {
                        if ((meshes[i].vertices[m].x == meshes[j].vertices[n].x) && (meshes[i].vertices[m].z == meshes[j].vertices[n].z))
                        {
                            if (meshes[i].vertices[m] != meshes[j].vertices[n])
                            {
                                meshes[i].vertices[m].y = meshes[j].vertices[n].y;
                            }
                        }
                    }
                }
            }
        }

它通过几百万个向量并将它们与所有其他向量进行比较,然后修改它们的一些 y 值。我认为它有效,但是在点击播放后,加载需要非常长的时间(目前已经等待 15 分钟,并且仍在继续)。有没有办法让它更有效率?感谢您的帮助!

【问题讨论】:

  • 并不是说这会使您的处理速度更快,但鉴于您的if 条件,您的第四个循环可能应该是for (int n = 0; n &lt; meshes[j].vertices.Length; n++)
  • @derpirscher 谢谢,我没有费心把正确的变量放在那里的原因是所有顶点数组的大小都相同,所以(据我所知)这不重要。跨度>
  • 能否请您描述一下初始问题?你有几百万个向量,你想得到(执行)什么?
  • 你的内在如果看起来不必要地昂贵。您是在说“如果 x 相同且 z 相同并且整个顶点不同,这可能会再次比较 x 和 z(我假设这些是结构)。你可以只比较那里的 y,但如果没有副作用,您可能只是跳过比较并分配 y。
  • 我不会尝试优化此循环,而是尝试生成您的网格,以便每个顶点的 y 坐标与其邻居相同。如果您知道地形的总长度和宽度,那么您应该能够计算给定网格的邻居在meshes 中的索引。使用这样的循环——即使你对其进行了优化——你的绝大多数比较都是在不是邻居的网格之间进行的,所以这都是浪费时间。

标签: c# for-loop unity3d optimization


【解决方案1】:

当我读到这篇文章时,您基本上在做的是,对于具有相同 xz 的所有顶点,您将它们的 y 值设置为相同。

更优化的方法是使用 Linq 方法 GroupBy,它在内部使用哈希映射来避免像您当前的方法那样的指数时间复杂度:

var vGroups = meshes.SelectMany(mesh => mesh.vertices)
    .GroupBy(vertex => new { vertex.x, vertex.z });
foreach (var vGroup in vGroups)
{
    vGroup.Aggregate((prev, curr) =>
        {
            // If prev is null (i.e. first iteration of the "loop")
            // don't change the 'y' value
            curr.Y = prev?.y ?? curr.y;
            return curr;
        });
}

// All vertices should now be updated in the 'meshes'

请注意,顶点的最终y 值取决于原始列表中网格和顶点的顺序。每个vGroup 中的第一个顶点是决定顶点。我相信这将与您的方法相反,即最后一个顶点是决定性的,但这听起来对您来说并不重要。

此外,请注意,如果两个顶点具有相同的 xz 值,则在此(和您的)方法中,您可能会合并同一网格中的两个顶点。我不知道这是不是故意的,但我想指出这一点。

另一个性能优化是并行化。只需致电AsParallel

var vGroups = meshes.AsParallel()
    .SelectMany(mesh => mesh.vertices)
    .GroupBy(vertex => new { vertex.x, vertex.z });
// ...

请注意,如果您尝试并行化的计算不是那么昂贵,那么并行化并不总能加快速度。并行化它的开销可能超过好处。我不确定GroupBy 操作是否足够重以使其有益,但您必须自己进行测试。先尝试不使用它。

有关简化示例,请参阅this fiddle

【讨论】:

  • 我正在尝试使用您显示的内容,但是我收到错误“操作员”?不能应用于“Vector3”类型的操作数。我真的不知道这种语法是如何工作的,所以我不知道如何解决它。有什么想法吗?
  • @TheWalrus 啊,这只是意味着prev 在您的情况下不可为空,因此您可以将curr.Y = prev?.y ?? curr.y; 替换为curr.Y = prev.y;。基本上prev?.y ?? curr.y 的意思是:“如果prev 不是 null,则给我prev.y,否则使用curr.y”。检查null-conditional operator
【解决方案2】:

您想让Y 对于具有相同XZ 的所有顶点相等。让我们这样做

var yForXZDict = new Dictionary<(int, int), int>();
foreach (var mesh in meshes)
{
    foreach (var vertex in mesh.vertices)
    {
        var xz = (vertex.x, vertex.z);
        if (yForXZDict.TryGetValue(xz, out var y))
        {
            vertex.y = y;
        }
        else
        {
            yForXZDict[xz] = vertex.y;
        }
    }
}

您应该将 int 替换为您用于坐标的确切类型

【讨论】:

  • 哇,这是一个如此简单和干净的解决方案,非常感谢!
【解决方案3】:

您不必要地比较了两次。

这里有一个简短的例子来说明我的意思:

假设我们有网格ABC

你在比较

  • A, A
  • A, B
  • A, C
  • B, A
  • B, B
  • B, C
  • C, A
  • C, B
  • C, C

虽然这会检查例如AB 的组合两次。


第一个简单的改进是使用例如

for (int i = 0; i < meshes.Count; i++)
{
    // only check the current and following meshes
    for (int j = i; j < meshes.Count; j++)
    {
        ...

您甚至想将网格与其自身进行比较吗?否则,您实际上甚至可以使用j = i + 1,因此只需将当前网格与下一个和后续网格进行比较。


然后取决于顶点。如果您实际上还想检查网格本身,至少您需要int n = m + 1,以防i == j

用自身检查一个顶点是没有意义的,因为条件总是为真。


下一点是最小化访问

您正在访问例如

meshes[i].vertices

五次!

宁可获取并存储一次,例如

// To minimize GC it sometimes makes sense to reuse variables outside of a loop
Mesh meshA;
Mesh meshB;
Vector3[] vertsA;
Vector3[] vertsB;
Vector3 vA;
Vector3 vB;
for (int i = 0; i < meshes.Count; i++)
{
    meshA = meshes[i];
    vertsA = meshA.vertices;

    for (int j = i; j < meshes.Count; j++)
    {
       meshB = meshes[j];
       vertsB = meshB.vertices;

       for(int m = 0; m < vertsA.Length; m++)
       {
           vA = vertsA[m];

           ...

还要注意像这样的一行

meshes[i].vertices[m].y = meshes[j].vertices[n].y;

实际上甚至不应该编译!

顶点是Vector3,这是一个struct,所以分配了

meshes[i].vertices[m].y

仅更改返回的Vector3 实例的值,但不应以任何方式更改数组的内容。

您宁愿使用前面提到的vA,最后将其分配回通过

vertsA[m] = vA;

然后在循环结束时通过

将整个数组分配回一次
meshA.vertices = vertsA;

最后好:我会将其放入 Thread 或使用 Unity 的 JobSystem 和突发编译器,同时例如显示进度条或一些用户反馈,而不是冻结整个应用程序。


还有一点是浮点精度

您正在使用== 直接比较两个float 值。由于浮点精度,即使不应该这样做也可能会失败,例如

10f * 0.1f == 1f

不一定是真的。可能是0.999999991.0000000001

因此,对于 Vector3 == Vector3,Unity 仅使用 0.00001 的精度。

您应该这样做并使用

if(Mathf.Abs(vA.x - vB.x) <= 0.00001f)`

或使用

if(Mathf.Approximately(vA.x, vB.x))

等于

if(Mathf.Abs(vA.x - vB.x) <= Mathf.Epsilon)`

Epsilon 是两个浮点数可以不同的最小值

【讨论】:

  • 哇,我没有意识到代码可以优化得这么好,非常感谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-02-18
  • 2021-02-26
  • 1970-01-01
  • 1970-01-01
  • 2016-05-24
  • 2017-01-27
  • 1970-01-01
相关资源
最近更新 更多