【问题标题】:Alternatives to Except and Union of Lists列表除外和联合的替代方案
【发布时间】:2013-09-22 16:03:30
【问题描述】:

很抱歉这篇长篇文章,感谢愿意花时间完成并给我反馈的人。

我有关于列表和数组操作的性能相关问题。

我编写了一个软件来对从一组传感器收集的数据执行一些操作。为了让它运行得更快,我目前正在尝试编写一些优化。

收集的数据位于 N×M 双精度数组中(实际上实现为扩展 List<List<double>> 的类)。对于 i 的任何值,我们总是有 this.Count() == Nthis[i].Count() == M。基本上,它是一个矩形数组。

此数组中的每个数据点都与 X-by-Y 地图上的一些点相关。基本上,想象这是我从数据中制作的图像,以快速清晰的方式表示它。因此,对于每个数据点,都有一个与其相关的地图点的List<int[]>。这个事实由List<int[]>[,] pointsLocal 表示。我还持有一个静态的List<int[]>[,],我在其中存储了相同的信息:这样我可以在精化循环中将pointsLocal 修改为我的闲暇时间,并在下次调用这些方法时拥有一个新副本。相同的传感器将始终与相同的点相关联,这就是我拥有该本地阵列的原因。有些点(实际上大部分)与多个传感器相关,因此出现在许多列表中。

在我的代码的其他部分,我能够正确识别出阵列的某些传感器存在问题,然后数据包含错误。这在private List<List<bool>> faultyData 中表示。如果传感器给出了错误的输出,那么我必须假设与之相关的所有点都可能遭受故障,因此我不在乎这些地图点的进一步分析结果。

我的代码的计算部分为每个地图点聚合来自阵列中所有传感器的数据。我正在尝试做的是预先确定我不需要执行任何分析的地图点子集。

PointsEqualityComparer 类是int[] 的自定义比较运算符,我使用它是因为地图点由它们的 2D 坐标标识。

public class Sinogram : List<List<double>>
{
    //various enums
   private List<List<bool>> faultyData; //faultyData[i][j] is true if there is an error in the data
    //constructors
    //some methods
    public void dataAnalysis()
    {
        List<int[]>[,] pointsLocal = new List<int[]>[this.Count(), this[0].Count()];
        List<int[]> faultyPoints = new List<int[]>();
        //Fill pointsLocal with the correlated points from the static array
        PointsEqualityComparer myComparer = new PointsEqualityComparer();
        //Point selection parts (see later for the two different implementations)
        //Actual analysis parts (not here because it is not relevant to my question, but it works)
    }
}

比较器类如下。我已经发现 GetHashCode 方法必须返回尽可能独特的结果以提高性能,所以我按照你在这个 sn-p 中看到的那样实现了它。

 public class PointsEqualityComparer : IEqualityComparer<int[]>
 {
    public bool Equals(int[] p1, int[] p2)
    {
        bool result = (p1.Count() == p2.Count()) && (p1.Count() == 2) && (p1[0] == p2[0]) && (p1[1] == p2[1]);
        return result;
    }

    public int GetHashCode(int[] obj)
    {
        return ((obj[0] + obj[1]) * (obj[0] + obj[1] + 1) + obj[1]);
    }
}

现在是棘手的部分。对于我实际选择有趣的地图点的代码部分,我有两种不同的实现。有趣的意思是我必须在其上聚合来自传感器的数据的地图点。我通过实际识别容易出错的点并将它们从列表中删除来选择它们。

在我的第一个实现中,我遍历所有地图点列表。如果相应的传感器有故障,我会将这些点添加到故障点列表中(避免重复)。一旦我遍历了所有点并生成了错误点的完整列表,我通过删除它们来更新allPairsLocal。 faultyPoints 的列表可能会变得相当大,尤其是在某些情况下,当很多传感器报告错误时(最大理论大小超过 2000000 个元素,如果所有传感器都报告错误并且我正在尝试创建一个 1920*1080 的地图来绘制作为高清图像)

for (int i = 0; i <this.Count; i++)
{
    for (int j = 0; j < this[i].Count; j++)
    {
        if (faultyData[i][j])
        {
            faultyPoints = faultyPoints.Union<int[]>(allPairsLocal[i, j], myComparer).ToList();
        }
    }
}
for (int i = 0; i <this.Count; i++)
{
    for (int j = 0; j < this[i].Count; j++)
    {
        allPairsLocal[i, j] = allPairsLocal[i, j].Except(faultyPoints, myComparer).ToList();
    }
}

在我的第二个实现中,我尝试使用更小的 faultyPoints 列表。因此,我所做的是,对于每个报告错误的传感器,使用它的列表从所有其他传感器(以及它自己的)中删除地图点。这样可以使列表的维度更小,但代价是更多的 annidated 循环。

for (int i = 0; i <this.Count; i++)
{
    for (int j = 0; j < this[i].Count; j++)
    {
        if (faultyData[i][j])
        {
            faultyPoints = allPairsLocal[i, j]. ToList();
            for (int x = 0; x < this.Count; x++)
            {
                for (int y = 0; y < this[x].Count; y++)
                {
                    allPairsLocal[x, y] = allPairsLocal[x, y].Except(faultyPoints, myComparer).ToList();
                }
            }
        }
    }
}

这两种实现都非常慢,我想这至少部分是因为数据集的大小。两者都比对整个数据集执行数据分析步骤花费的时间更长。 有没有办法进行类似的操作但实现速度更快?有些步骤可能是平行的,但这并不会真正改变实质。是否存在具有 O(1) 方法的数据结构来实现我在这里使用 Union 和 except 所做的事情?

再次感谢您阅读我的整篇文章。我很感激任何反馈,即使它不是一个完整的答案,我也很乐意澄清我可以澄清的点。

【问题讨论】:

    标签: c# performance list union except


    【解决方案1】:

    如果我对您的理解正确,一旦您填充了 pointsLocal 数组,我们就会为每个传感器 (i,j) 提供以下信息:

    • this[i][j] = 来自传感器 (i,j) 的数据
    • pointsLocal[i,j] = 传感器 (i,j) 的地图点列表
    • faultyData[i][j] = 如果来自传感器 (i,j) 的数据不正确,则为 true,否则为 false

    考虑“反转”您的数据,以便给定地图点 (x,y) 您可以有效地

    • 查明该点是否有故障(即地图点的任何传感器报告数据是否有故障)
    • 获取报告与地图点相关的数据的传感器列表

    为此,我们可以创建一个使用您已经编写的比较器的字典。每个键都是一个(x,y) 对(即一个int[2]),代表一个地图点;返回的值(如果有)是对该点有贡献的已知传感器列表。返回值null 表示地图点被有故障的传感器“感染”,应该被忽略。如果字典中根本不存在给定的对,则意味着没有传感器对该点有贡献。

    var mapPoints = new Dictionary<int[], List<int[]>)(PointsEqualityComparer);
    
    for (int i = 0; i <this.Count; i++)
    {
        for (int j = 0; j < this[i].Count; j++)
        {
            foreach (var point in pointsLocal[i,j]) 
            {
                if (faultyData[i][j])
                {
                    // infected point
                    mapPoints[point] = null;  
                }
                else
                {
                    // Add the current sensor's indices (i,j) to the list of 
                    // known sensors for the current map point
    
                    List<int[]> sensors = null;
                    if (!mapPoints.TryGetValue(point, out sensors)) 
                    {
                        sensors = new List<int[]>();
                        mapPoints[point] = sensors;
                    }
    
                    // null here means that we previously determined that the
                    // current map point is infected 
                    if (sensors != null) 
                    {
                        // Add sensor to list for this map point
                        sensors.Add(new int[] { i, j });
                    }
                }
            }
        } 
    }
    

    现在您可以枚举所有地图点,将每个点分类为好或坏:

    var faultyPoints = new List<int[]>();  // not sure you really need this? 
    var goodPoints = new List<int[]>();
    foreach (var point in mapPoints.Keys)
    {
        var sensors = mapPoints[point];
        if (sensors == null)
             faultyPoints.Add(point);
        else
             goodPoints.Add(point);
    }
    

    最后,您可以为每个好的地图点枚举传感器,以进行分析:

    foreach (var point in goodPoints) 
    {
        var sensors = mapPoints[point]; 
        // for current point, aggregate data for each sensor listed in "sensors"
    }
    

    请注意,我没有更改allPairsLocal,因为分析步骤似乎没有必要。但是,如果您确实需要从中删除错误的地图点,您也可以有效地做到这一点:

    for (int i = 0; i <this.Count; i++)
    {
        for (int j = 0; j < this[i].Count; j++)
        {
            var points = allPairsLocal[i][j];
            var cleanedUp = new List<int[]>();
            foreach (var point in points) 
            {
                // Important: do NOT use 'faultyPoints' here. It will kill performance
                if (mapPoints[point] != null)
                {
                   cleanedUp.Add(point); 
                }
            }
            allPairsLocal[i][j] = cleanedUp;   
        }
    }
    

    所有这一切的性能改进都来自于使用字典来查找一个单个地图点,无论何时您需要知道它是否有故障或其贡献的传感器是什么。如果您的哈希函数良好,则查找本质上是一个恒定时间操作(摊销)。

    您可以在此处进行许多优化。例如,您真的需要知道传感器索引来为每个地图点进行聚合吗?还是您只需要数据值?如果是后者,那么您的字典将是Dictionary&lt;List&lt;double&gt;&gt;。最后,通过使用 Linq(而不是循环)进行许多枚举,可以使代码更加紧凑。

    【讨论】:

    • 感谢您详尽的回复。早上我会尝试,但看起来应该可以。
    • Quick cmets:在字典的声明中,我的视觉工作室希望我使用 new PointsEqualityComparer() 而不是类名。除此之外,它似乎可以工作。谢谢。
    • 我无法从您的描述中判断每个地图点是否至少有一个相关传感器。如果是这样,那么您实际上可以完全放弃字典并使用二维数组来表示与字典相同的信息。这将比使用字典更快,并且根本不需要相等比较器 / has 函数。祝你好运
    • 我实际上刚刚刷新了这个页面,说我选择用二维数组替换字典,因为经过一些测试,我发现相等比较器和哈希函数引入的开销大于优化带来的收益
    【解决方案2】:

    是的,你是对的。这是因为联合和除外操作的复杂性。
    您有 N×M 传感器表(您在上面将其命名为 Lists of map-points)。每个传感器都会影响一组点(您将其命名为allPairsLocal[i, j])。每个点数组都是全局预定点数组的子集 (points on a X-by-Y map)。
    如果我是对的,那么:

    1. X-by-Y 地图上的点 - 这是一个全局点数组。更重要的是,由于您可以比较点,您可以对它们进行排序并保持这个数组排序(我的意思是可能实际上没有排序,但具有良好的读取操作复杂性)。使用Dictionary&lt;int[], int&gt; 表示关键点坐标、值顺序索引(插入所有点后设置)。
    2. 现在我们有了一组传感器(让我们将第 1 步中的 Dictionary&lt;int[], int&gt; 命名为点)。我们需要构建 2 个映射 - 一个 sensors2points(命名为 s2p)和 points2sensors(命名为 p2s)。你有allPairsLocal 作为sensors2points,看起来像List&lt;int[]&gt;[][],即每个传感器的点坐标列表。但是我们需要将索引列表保留为每个传感器的点坐标,即将int[] 转换为points 中的顺序索引:

      // straight and inverted mappings
      var s2p = new List<int>[N*M];
      var p2s = new List<List<int>>(point.Count);
      //and initialize p2s inner lists
      for (int i = 0; i < p2s.Count; i++)
          p2s[i] = new List<int>();
      
      for (int i = 0; i < N * M; i++)
      {
          s2p[i] = new List<int>(allPairsLocal[i/M][i%M].Count);
      
          //convert list of points coordinates to list of it's indices
          // and construct inverted mapping
          foreach(int[] p in allPairsLocal[i/M][i%M])
          {
              // points[p] - index of point p in Dictionary if you remember
              s2p[i].Add(points[p]);
              p2s[points[p]].Add(i);
          }            
      }
      

    我认为很明显,步骤 1 和 2 只需要在初始化时执行一次。然后选择您需要的有趣点:

    //I don't know which set you need as a result - valid points or sensors so I do both
    
    // false - correct, true - error. Initialized with false
    BitArray sensorsMask = new BitArray(sensors.Count);
    BitArray pointsMask = new BitArray(points.Count);
    
    for (int i = 0; i < N * M; i++)
    {
        if (faultyData[i / M][i % M])
            sensorsMask[i] = true; // means error in sensor
    
        foreach(int p in s2p[i])
            pointsMask[p] = true;
    }
    
    // so you can get only valid sensors
    var validSensors = new List<int>();
    for (int i = 0; i < N * M; i++)
        if (!sensorsMask[i])
            validSensors.Add(i);
    
    // or only valid points
    var validPoints = new List<int[]>();
    foreach (var pair in points)
        if (!pointsMask[pair.Value])
            validPoints.Add(points.Key);
    

    这可能不是很有效的方式(很难说出你到底想得到什么),但它比使用集合更好。我的意思是玩 mask-array vs sets。希望对您有所帮助。

    【讨论】:

    • 我可能最终实现了与您建议的数组类似的东西,因为我没有使用字典,而是使用 bool[,] badPoints 来跟踪我必须执行后续步骤的地图点分析
    猜你喜欢
    • 1970-01-01
    • 2011-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-10
    • 1970-01-01
    相关资源
    最近更新 更多