在C#中优化稀疏点积

时间:2010-05-10 12:42:41

标签: c# optimization

我正在尝试计算两个非常稀疏的关联数组的点积。 数组包含ID和值,因此只应对两个数组共有的ID进行计算,例如。

[(1, 0.5), (3, 0.7), (12, 1.3)] * [(2, 0.4), (3, 2.3), (12, 4.7)] = (0.7 * 2.3) + (1.3 * 4.7)

我的实现(称之为 dict )目前使用的是词典,但它的速度太慢了。

double dot_product(IDictionary<int, double> arr1, IDictionary<int, double> arr2)
  {
     double res = 0;
     double val2;
     foreach (KeyValuePair<int, double> p in arr1)
        if (arr2.TryGetValue(p.Key, out val2))
           res += p.Value * val2;
     return res;
  }

完整数组每个都有大约500,000个条目,而稀疏数组每个只有几十到几百个条目。

我做了一些玩具版点产品的实验。 首先,我尝试将两个双阵列相乘以查看我可以获得的最终速度(让我们称之为“平面”)。

然后我尝试使用int[] ID数组double[] 值数组来改变关联数组乘法的实现,一起走在 ID数组上并在它们相等时相乘(让我们称之为“ double ”)。

然后我尝试使用 F5 Ctrl - F5 运行所有三个版本的调试或发布。 结果如下:

debug F5:    dict: 5.29s double: 4.18s (79% of dict) flat: 0.99s (19% of dict, 24% of double)
debug ^F5:   dict: 5.23s double: 4.19s (80% of dict) flat: 0.98s (19% of dict, 23% of double)
release F5:  dict: 5.29s double: 3.08s (58% of dict) flat: 0.81s (15% of dict, 26% of double)
release ^F5: dict: 4.62s double: 1.22s (26% of dict) flat: 0.29s ( 6% of dict, 24% of double)

我不明白这些结果 为什么版本 F5 中的字典版本不像双版和平版一样优化?
为什么在版本^ F5 版本中只略微优化,而其他两个版本进行了大量优化?

此外,由于将我的代码转换为“double”方案意味着很多工作 - 您是否有任何建议如何优化字典?

谢谢!

4 个答案:

答案 0 :(得分:3)

我建议使用SortedList<int, double>代替词典。您现在可以创建两个单独的枚举器并并行遍历每个列表,而不是重复运行TryGetValue。始终使用枚举中的“后面”列表向前移动,只要您看到两个枚举元素相等,就会找到匹配项。暂时没有我的IDE,但伪代码是这样的:

Get enumerator for vector A
Get enumerator for vector B
while neither enumerator is at the end
   if index(A) == index(B) then
     this element is included in dot product
     move forward in A and B
   else if index(A) < index(B) then
     move forward in A
   else # index(A) > index(B)
     move forward in B
continue while loop

答案 1 :(得分:1)

谢谢大家。 我决定将代码转换为在排序数组上使用并行遍历(“双”方法),使用正确的包装器并没有花费太多时间来转换,因为我担心它会。 显然,JIT /编译器优化对泛型的效果不如数组那么好。

答案 2 :(得分:0)

我认为您无法对dot_product函数进行更多优化。您必须运行一个字典并检查第二个字典是否包含任何 ID 。也许你可以实现一些检查,哪个字典的大小更小,并在这个字典上执行foreach。如果两者的大小可能会有很大差异(例如arr1 = 500000arr2 = 1000),则可以为您提供额外的效果。

但如果您认为这仍然太慢,那么性能影响可能不会来自此功能。也许更大的问题是字典的创建和填充。所以也许你可以通过使用简单的数组方法更好。但这取决于您为函数创建必要结构的频率。您是否需要在每次需要时从头开始创建这些词典,或者它们是否会在启动时创建和填充,之后的任何更改都会直接反映到这些结构中?

为了得到你问题的好答案(你自己),你不仅要检查你的算法(这对我来说似乎相当快),还需要多长时间来创建和维护这个功能的必要基础设施以及如何这些费用是多少?

更新

阅读完评论后,我无法真正理解为什么这种方法如此缓慢(不使用探查器;-))。通常TryGetValue应该在O(1)附近执行,计算本身也不是那么难。所以唯一的办法是优化foreach运行。但是由于事实,有人必须遍历所有项目,你只能在这个步骤中选择两个中最短的一个(如已经提到的那样)。“

从这个方面来看,我看不到你能做的任何事情。

答案 3 :(得分:0)

你可以试试这个很快。定义一个结构,如:

public struct MyDoubles
{
    public Double Val1 { get; set; }
    public Double Val2 { get; set; }
    public Double Product()
    {
        return Val1 * Val2;
    }
}

并定义一个数组,只要最大的id。

MyDoubles[] values = new MyDoubles[1000000];

然后使用id作为索引位置,用array1和Val2中的值填充Val1,其值为array2。

然后循环并计算:

public double DotProduct2(MyDoubles[] values)
{
    double res = 0;
    for (int i = 0; i < values.Length; i++)
    {
        res += values[i].Product();
    }
    return res;
}

根据您的最大ID,您可能会遇到内存问题,并且还需要设置数据结构。

我使用字典版本与我提出的数组/结构版本进行计算的时间产生了这些数字:

Dict: 5.38s
Array: 1.87s

[使用发布版本进行更新]

Dict: 4.70s
Array: 0.38s