首页 > 解决方案 > 在结构性能影响中包装 C# 原语

问题描述

我正在写一些关于几何处理的代码,更具体的 delaunay 三角剖分,我需要它快速,所以我使用简单的原始数组作为数据结构来表示我的三角剖分信息,这里是它的一个示例

        private readonly float2[] points;
        private readonly int[] pointsHalfEdgeStartCount;
        private readonly int[] pointsIncomingHalfEdgeIndexes;

因此,假设我想快速遍历索引 p 点的所有传入半边,我只是使用预先计算的数组来执行此操作:

int count = pointsHalfEdgeStartCount[p * 2 + 1];

for (int i = 0; i < count; i++)
{
    var e = pointsIncomingHalfEdgeIndexes[pointsHalfEdgeStartCount[p * 2] + i]
}

// pointsHalfEdgeStartCount[p * 2] is the start index

这足够快,但感觉不安全或非常清楚。所以我想到了将我的索引包装到结构中以使其更清晰,同时保持性能,就像这样:

public readonly struct Point
{
    public readonly int index;
    public readonly DelaunayTriangulation delaunay

    public Point(int index, DelaunayTriangulation delaunay)
    {
        this.index = index; 
        this.delaunay = delaunay;
    }

            
    public int GetIncomingHalfEdgeCount() => delaunay.pointsEdgeStartCount[index * 2 + 1];
    public HalfEdge GetIncomingHalfEdge(int i)
    {
        return new HalfEdge(
            delaunay,
            delaunay.pointsIncomingHalfEdgeIndexes[delaunay.pointsEdgeStartCount[index * 2] + i]
        );
    }

    //... other methods
}

然后我可以这样做:

int count = p.GetIncomingHalfEdgeCount();

for (int i = 0; i < count; i++)
{
    var e = p.GetIncomingHalfEdge(i);
}

然而,这有点扼杀我的表现,在我所做的基准测试中速度要慢得多(大约 10 倍),迭代所有点并迭代所有传入的半边。我猜是因为在每个点结构中存储对 delaunay triangulaiton 的引用是一种明显的浪费,并且减慢了所有涉及点的操作,需要移动的数据量是原来的两倍。

我可以将 DelaunayTriangulation 设为静态类,但由于其他原因不实用,所以我这样做了:

public readonly struct Point
{
    public readonly int index;
    
    public Point(int index) => this.index = index;

            
    public int GetIncomingHalfEdgeCount(DelaunayTriangulation delaunay) => delaunay.pointsEdgeStartCount[index * 2 + 1];
    public HalfEdge GetIncomingHalfEdge(DelaunayTriangulation delaunay, int i)
    {
        return new HalfEdge(
            delaunay.pointsIncomingHalfEdgeIndexes[delaunay.pointsEdgeStartCount[index * 2] + i]
        );
    }

    //... other methods
}

我可以这样做:

int count = p.GetIncomingHalfEdgeCount(delaunay);

for (int i = 0; i < count; i++)
{
    var e = p.GetIncomingHalfEdge(delaunay, i);
}

它快了很多,但仍然比使用简单 int 的第一种方法慢 2.5 倍。我想知道这是否可能是因为我在第一种方法中得到 int 而在其他方法中得到 HalfEdge 结构(类似于 Point 结构的结构,只包含一个索引作为数据和几个方法),以及普通方法之间的区别当我使用 e int 实例化一个新的 HalfEdge 结构时, int 和更快的结构消失了。虽然我不确定为什么这么贵。更奇怪的是,为了清楚起见,我探索了在 Delaunay 类而不是 Point 结构中编写方法的选项:

// In the DelaunayTriangulation class:

public int GetPointIncomingHalfEdgeCount(Point p) => pointsEdgeStartCount[p.index * 2 + 1];
public HalfEdge GetPointIncomingHalfEdge(Point p, int i)
{
    return new HalfEdge(
        pointsIncomingHalfEdgeIndexes[pointsEdgeStartCount[p.index * 2] + i]
        );
}

我这样使用它:

int count = delaunay.GetPointIncomingHalfEdgeCount(p);

for (int i = 0; i < count; i++)
{
    var e = delaunay.GetPointIncomingHalfEdge(p, i);
}

而且比之前的方法慢了3倍!我不知道为什么。

我尝试使用反汇编来查看生成了哪些机器代码,但我没有这样做(我正在使用 Unity3D)。我是否注定要依赖数组中的普通 int 和健全的变量命名,并放弃尝试进行一些编译时类型检查(这个 int 真的是点索引吗?)

我什至没有提出其他问题,例如,为什么当我尝试使用具有如下产量的 IEnumerable 类型时它会更慢:

public IEnumerable<int> GetPointIncomingHalfEdges(Point p)
{
    int start = pointsEdgeStartCount[p.index * 2]; // this should be a slight optimization right ?
    int count = pointsEdgeStartCount[p.index * 2 + 1];
    for (int i = 0; i < count; i++)
    {
        yield pointsIncomingHalfEdgeIndexes[start + i];
    }
}

标签: c#unity3d

解决方案


我添加了一个用于积极内联的编译器指令,它似乎及时弥补了差异!由于某种原因,编译器无法正确内联:

var e = delaunay.GetPointIncomingHalfEdge(p, i);

虽然它设法做到了

var e = p.GetIncomingHalfEdge(delaunay, i);

为什么 ?我不知道。但是,如果我能够看到代码是如何编译的并且我找不到如何做到这一点,那就容易多了。我会搜索它,也许会打开另一个问题,如果我找到更好的解释,我会回来的!


推荐阅读