使用 Unity Jobs 和 Burst 进行寻路比不使用时慢

问题描述 投票:0回答:1

我尝试根据视频并改进其代码,在 Unity 中实现游戏的寻路。为了提高多线程的效率,我想实现 Unity Jobs,但由于这是我第一次这样做,我得到了一些有趣的结果。

使用下面的代码,我的性能比没有工作时差得多,并且崩溃了,我不明白为什么。在分析器上(在 Unity 编辑器中进行分析)至少需要 14 毫秒才能找到新路径,这比应有的情况要糟糕得多,因为没有作业时速度要快得多。这仅是两个单位在节点数低于 1000 的关卡上对同一目标进行寻路的情况。由于我在互联网上找到了同一寻路主题的代码,因此我认为它应该具有更高的性能。即使在这些情况下,观看视频的性能影响也应低于 1 毫秒。我发现甚至 github 仓库都有更大的地图和超过 100 个代理,性能也好得多。

也许我不理解作业系统的某些内容,或者在代码中的某个地方犯了错误,但无法弄清楚是什么导致了性能下降。

首先,我有一个单元,每次目标对象移动一定距离并在指定时间后调用 PathRequestManager。这将创建一个新的 PathRequest,其中包含当前位置、目标位置、计算路径时应调用的回调以及当前单位。然后在一个方法中,我将请求的位置添加到单独的 NativeList 中。这些将包含应在下一个 Update() 中调用的路径请求参数。

在更新中,我查看是否应该使用 shouldDoPathFinding 变量来完成寻路,当添加新路径请求时,该变量设置为 true。如果是真的,我调用 StartFindPathJobifiedHash 方法。将所需的本机数组、哈希映射、多哈希映射传递给作业并调度它。

public void StartFindPathJobifiedHash()
    {
        CopyNextAttributesToCurrent(); //Copies the next pathfinding attributes to the current list
        ClearNextPathfindingAttributes(); //Clears next pathfinding attributes
 
       //unitList contains the units which should do pathfinding in current Update
        for (int i = 0; i < unitList.Count; i++)
        {
            NodeStruct startNode = grid.NodeFromWorldPointJobified(startPosList[i]);
            int startNodeID = startNode.nodeID;
            NodeStruct endNode = grid.NodeFromWorldPointJobified(endPosList[i]);
            int endNodeID = endNode.nodeID;
 
//startNodeList and endNodeList are native lists
            startNodeList.Add(startNodeID);
            endNodeList.Add(endNodeID);
        }
     
       //node neighbours are calculated when grid is created on main thread
        NativeParallelMultiHashMap<int, int> neighboursMap = grid.neighboursMapJobified;
 
        //originalPathNodeArray is a native array which contains all the nodes in the grid, populated when the grid is calculated
        // neighboursMap is a native parallel multi hashmap, with node ids as keys and neighbour ids as values, populated when the grid is calculated
        // waypointsHash native parallel multi hashmap with node ids as keys and positions as values
        FindPathJobParallel findPathJob = new FindPathJobParallel { pathRequestIDList = pathRequestIDList, originalPathNodeArray = originalPathNodeArray, neighboursMap = neighboursMap, startNodeList = startNodeList, endNodeList = endNodeList, waypointsHash = waypointsHash, pathSuccessHash = pathSuccessHash, originalPathNodeArrayHash = originalPathNodeHash, unitIDs = unitIDs };
     
        JobHandle jobHandle = findPathJob.Schedule(unitList.Count, 1);
        jobhandle = jobHandle;
    }

这是工作:

[BurstCompile]
public struct FindPathJobParallel : IJobParallelFor
{
 
 
 
    [ReadOnly, NativeDisableParallelForRestriction] public NativeArray<NodeStruct> originalPathNodeArray;
    [ReadOnly, NativeDisableParallelForRestriction] public NativeParallelHashMap<int, NodeStruct> originalPathNodeArrayHash;
 
    [ReadOnly, NativeDisableParallelForRestriction] public NativeList<int> pathRequestIDList;
    [ReadOnly, NativeDisableParallelForRestriction] public NativeList<int> startNodeList;
    [ReadOnly, NativeDisableParallelForRestriction] public NativeList<int> endNodeList;
    [ReadOnly, NativeDisableParallelForRestriction] public NativeList<int> unitIDs;
 
    [WriteOnly, NativeDisableParallelForRestriction] public NativeParallelMultiHashMap<int, float3> waypointsHash;
    [NativeDisableParallelForRestriction] public NativeParallelHashMap<int, bool> pathSuccessHash;
    [ReadOnly, NativeDisableParallelForRestriction] public NativeParallelMultiHashMap<int, int> neighboursMap;
 
 
 
 
    public void Execute(int i)
    {
 
        NativeList<NodeStruct> openList = new NativeList<NodeStruct>(Allocator.Temp);
 
        NativeList<NodeStruct> closedList = new NativeList<NodeStruct>(Allocator.Temp);
 
 
 
        NativeParallelMultiHashMap<int, int> openHashMap = new NativeParallelMultiHashMap<int, int>(100, Allocator.Temp);
        NativeParallelMultiHashMap<int, int> closedHashMap = new NativeParallelMultiHashMap<int, int>(1000, Allocator.Temp);
 
 
        NativeList<int> neighbourIndexes = new NativeList<int>(10, Allocator.Temp);
 
        NativeArray<NodeStruct> pathNodeArray = new NativeArray<NodeStruct>(originalPathNodeArray.Length, Allocator.Temp);
        NativeParallelHashMap<int, NodeStruct> pathNodeHash = new NativeParallelHashMap<int, NodeStruct>(originalPathNodeArray.Length, Allocator.Temp);
 
 
        NodeStruct startNodeStruct = originalPathNodeArray[0];
        NodeStruct endNodeStruct = originalPathNodeArray[0];
 
        NativeParallelHashMap<int, bool> successList = new NativeParallelHashMap<int, bool>(100, Allocator.Temp);
 
 
        NodeStruct nodeStruct = new NodeStruct();
        nodeStruct = originalPathNodeArrayHash[startNodeList[i]];
        startNodeStruct = nodeStruct;
 
        startNodeStruct.parentID = startNodeStruct.nodeID;
 
 
 
        NodeStruct endNodeStructNew = new NodeStruct();
        endNodeStructNew = originalPathNodeArrayHash[endNodeList[i]];
        endNodeStruct = endNodeStructNew;
 
        if (!pathNodeHash.ContainsKey(startNodeList[i]))
        {
            pathNodeHash.Add(startNodeList[i], startNodeStruct);
        }
        if (!pathNodeHash.ContainsKey(endNodeList[i]))
        {
            pathNodeHash.Add(endNodeList[i], endNodeStruct);
        }
 
 
 
        startNodeStruct.gCost = 0;
     
        openList.Add(startNodeStruct);
        openHashMap.Add(startNodeStruct.nodeID, startNodeStruct.nodeID);
     
        while (openList.Length > 0)
        {
            NodeStruct currentNodeStruct = GetLowestCostFNodeIndex(openList, openHashMap);
         
            closedList.Add(currentNodeStruct);
            closedHashMap.Add(currentNodeStruct.nodeID, currentNodeStruct.nodeID);
            if (currentNodeStruct.nodeID == endNodeStruct.nodeID)
            {
             
                pathSuccessHash[unitIDs[i]] = true;
                successList[i] = true;
                break;
            }
 
       
            var neighboursOnIndexEnumerator = neighboursMap.GetValuesForKey(currentNodeStruct.nodeID);
 
            while (neighboursOnIndexEnumerator.MoveNext())
            {
             
                neighbourIndexes.Add(neighboursOnIndexEnumerator.Current);
            }
 
            NodeStruct neighbourStruct = originalPathNodeArray[0];
         
            for (int neighbourIndex = 0; neighbourIndex < neighbourIndexes.Length; neighbourIndex++)
            {
             
                int currentNeighbourIndex = neighbourIndexes[neighbourIndex];
                neighbourStruct = originalPathNodeArray[currentNeighbourIndex];
 
 
                if (!neighbourStruct.walkable)
                {
                    continue;
                }
 
                if (closedHashMap.ContainsKey(currentNeighbourIndex))
                {
                    continue;
                }
 
 
                int newMovementCostToNeighbour = currentNodeStruct.gCost + (int)GetDistanceJobified(currentNodeStruct.worldPosition, neighbourStruct.worldPosition, currentNodeStruct.gridX, currentNodeStruct.gridY, neighbourStruct.gridX, neighbourStruct.gridY) + neighbourStruct.movementPenalty;
                if (newMovementCostToNeighbour < neighbourStruct.gCost || !openHashMap.ContainsKey(currentNeighbourIndex))
                {
                    NodeStruct newNodeStruct = new NodeStruct();
                    newNodeStruct.gridX = neighbourStruct.gridX;
                    newNodeStruct.gridY = neighbourStruct.gridY;
                    newNodeStruct.walkable = neighbourStruct.walkable;
                    newNodeStruct.worldPosition = neighbourStruct.worldPosition;
                    newNodeStruct.movementPenalty = neighbourStruct.movementPenalty;
                    newNodeStruct.walkable = neighbourStruct.walkable;
                    newNodeStruct.gCost = newMovementCostToNeighbour;
                    newNodeStruct.hCost = (int)GetDistanceJobified(neighbourStruct.worldPosition, endNodeStruct.worldPosition, neighbourStruct.gridX, neighbourStruct.gridY, endNodeStruct.gridX, endNodeStruct.gridY);
                    newNodeStruct.nodeID = neighbourStruct.nodeID;
                    newNodeStruct.parentID = currentNodeStruct.nodeID;
                    newNodeStruct.angle = neighbourStruct.angle;
                    newNodeStruct.normal = neighbourStruct.normal;
                    newNodeStruct.modifiedWorldPosition = neighbourStruct.modifiedWorldPosition;
 
                    if (!pathNodeHash.ContainsKey(newNodeStruct.nodeID))
                    {
                        pathNodeHash.Add(newNodeStruct.nodeID, newNodeStruct);
                    }
                    else
                    {
                        pathNodeHash[newNodeStruct.nodeID] = newNodeStruct;
                    }
                 
                    if (!openHashMap.ContainsKey(currentNeighbourIndex))
                    {
                        openList.Add(neighbourStruct);
                        openHashMap.Add(neighbourStruct.nodeID, neighbourStruct.nodeID);
                     
                    }
                }
 
            }
 
        }
 
        if (pathSuccessHash[unitIDs[i]])
        {
         
            NativeList<float3> waypointsList = new NativeList<float3>(100, Allocator.Temp);
            waypointsList = RetracePathJobifiedHash(originalPathNodeArrayHash[startNodeStruct.nodeID], pathNodeHash[endNodeStruct.nodeID], pathNodeHash);
 
            for (int j = 0; j < waypointsList.Length; j++)
            {
                waypointsHash.Add(unitIDs[i], waypointsList[j]);
             
                pathSuccessHash[unitIDs[i]] = waypointsList.Length > 0;
            }
 
 
 
            waypointsList.Dispose();
        }
     
 
        openList.Dispose();
        closedList.Dispose();
        pathNodeArray.Dispose();
        neighbourIndexes.Dispose();
 
    }
 
 
 
    public NodeStruct GetLowestCostFNodeIndex(NativeList<NodeStruct> openList, NativeParallelMultiHashMap<int, int> openHashMap)
    {
        NodeStruct lowestCostNode = openList[0];
 
        int lowestIndex = 0;
 
        for (int i = 0; i < openList.Length; i++)
        {
            NodeStruct currentCostNode = openList[0];
            if (currentCostNode.fCost < lowestCostNode.fCost)
            {
                lowestCostNode = currentCostNode;
                lowestIndex = i;
            }
        }
        openList.RemoveAt(lowestIndex);
        openHashMap.Remove(lowestCostNode.nodeID);
        return lowestCostNode;
 
    }
 
    float GetDistanceJobified(float3 nodeAPos, float3 nodeBPos, int nodeAGridX, int nodeAGridY, int nodeBGridX, int nodeBGridY)
    {
 
        int dstX = math.abs(nodeAGridX - nodeBGridX);
        int dstY = math.abs(nodeAGridY - nodeBGridY);
        float dstZ = math.abs(nodeAPos.y - nodeBPos.y);
 
        if (dstZ != 0)
        {
         
            dstZ = dstZ * 10;
        }
        if (dstX > dstY)
            return 14 * dstY + 10 * (dstX - dstY) + (int)dstZ;
        return 14 * dstX + 10 * (dstY - dstX) + (int)dstZ;
     
    }
 
    NativeList<float3> RetracePathJobifiedHash(NodeStruct startNode, NodeStruct endNode, NativeParallelHashMap<int, NodeStruct> pathNodeArray)
    {
     
 
        NativeList<NodeStruct> path = new NativeList<NodeStruct>(100, Allocator.Temp);
        NodeStruct currentNode = endNode;
 
        while (currentNode.nodeID != startNode.nodeID)
        {
         
            path.Add(currentNode);
            currentNode = pathNodeArray[currentNode.parentID];
 
        }
 
        NativeList<float3> pathVector3List = new NativeList<float3>(100, Allocator.Temp);
 
        for (int i = 0; i < path.Length; i++)
        {
            pathVector3List.Add(path[i].worldPosition);
        }
 
     
        NativeList<float3> waypoints = new NativeList<float3>(pathVector3List.Length, Allocator.Temp);
        for (int i = 0; i < path.Length; i++)
        {
            waypoints.Add(path[i].worldPosition);
        }
 
        path.Dispose();
        pathVector3List.Dispose();
 
        pathNodeArray.Dispose();
        return waypoints;
    }
 
 
}

这项工作是一个简单的 A* 寻路脚本,用原生列表和数组重写。我在开始时创建网格节点并在每次寻路时使用它。为了不覆盖节点,当发现新节点时,我创建一个新的节点结构并将其用于当前作业的未来。它工作得很好,但我不明白为什么一项工作至少需要 14 毫秒才能完成。在探查器中,我可以看到多个线程正在工作。

您能看一下代码并尝试告诉我问题出在哪里吗?尝试对代码进行注释以使其更容易理解,但如果有任何问题,我很乐意回答。

编辑:使用Unity 2021.3.16版本,尝试关闭突发选项中的安全检查,性能是相同的。当我关闭作业时,寻路工作会在 1 毫秒内完成。问题应该出在 FindPathJobParallel 代码中,就像在分析器中花费最多时间一样。

c# performance unity-game-engine jobs unity-burst
1个回答
0
投票

Burst 编译器做了一件事——它很好地优化了数学运算。但 Burst 对您设计的数据布局和内存访问模式没有影响。这意味着 3 个 CPU 性能因素中有 2 个掌握在程序员手中。

内存数据布局

NodeStruct
是结构太大而且太折衷。它至少是 84 字节,而 Chubby
Matrix4x4
是 64 字节(!)。这会破坏整个程序,因为 17 个数据集合中有 6 个使用了该结构。这种情况会无缘无故地进行 CPU 复制并移动这些大块结构。解决方案是将这个单个结构体拆分为数组集合 (
NativeArray<Cost> costs
,
NativeArray<ID> ids
,
NativeArray<float3> worldPositions
, ...)

内存访问模式

归根结底是为了避免不可预测的随机访问模式。

https://en.wikipedia.org/wiki/Memory_access_pattern

© www.soinside.com 2019 - 2024. All rights reserved.