C# C语言中稀疏点积的优化#

C# C语言中稀疏点积的优化#,c#,optimization,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, IDiction

我试图计算两个非常稀疏的关联数组的点积。 数组包含一个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;
  }
我不理解这些结果。
为什么F5版中的词典版本没有像双版本和扁平版本那样进行优化?
为什么在发布版^F5中只对其进行了轻微优化,而其他两个版本则进行了大量优化

另外,由于将我的代码转换为“double”方案将意味着大量的工作,您对如何优化dictionary one有什么建议吗

谢谢

Haggai

我认为你不能再优化你的dot_产品功能了。您必须运行一个字典并检查第二个字典是否包含这些ID。也许您可以执行一些检查哪个字典的大小更小,并在此字典上执行
foreach
。如果两者的大小可以大幅度变化(例如,
arr1=500000
arr2=1000
),则这可以为您提供一些额外的性能

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

为了(从你自己那里)得到一个好的答案,你不仅应该检查你的算法(对我来说似乎很快),还应该检查为这个功能创建和维护必要的基础设施需要多少时间,以及这些功能的成本有多高

更新 在阅读了您的评论之后,我真的不明白为什么这个方法这么慢(不使用分析器;-)。通常情况下,
TryGetValue
应该在
O(1)
附近执行,计算本身也没有那么困难。因此,唯一的事情就是优化
foreach
运行。但是,由于有人必须迭代所有项目,因此在这一步中选择两个项目中最短的一个(如前所述),您只能将其缩短一点


除此之外,我看不出你还能做什么。

我建议使用
SortedList
而不是字典。您现在可以创建两个单独的枚举数并并行遍历每个列表,而不是重复运行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
获取向量A的枚举数
获取向量B的枚举数
而两个枚举数都不在末尾
如果指数(A)=指数(B),则
此元素包含在dot产品中
在A和B中向前移动
否则,如果索引(A)<索引(B),则
前进
else#索引(A)>索引(B)
在B中前进
边循环边继续
谢谢大家。 我已经决定将代码转换为使用排序数组上的并行遍历(“double”方法),使用正确的包装器转换代码不会像我担心的那样花费太多时间。
显然,JIT/编译器优化对泛型的效果不如对数组的效果好。

你可以试试这个,速度非常快。定义一个结构,如:

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中的值填充Val1,用array2中的值填充Val2

然后循环并计算:

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

Dict: 4.70s
Array: 0.38s

谢谢我做了一些分析,这种方法(与另一种非常类似的方法一起)是我的程序性能低下的罪魁祸首。字典是预先填充的,即使在我上面提到的实验中,我也只测量了乘法的执行时间,没有数据填充时间。乘法数组的大小大致相同,但我还是尝试了您的建议-执行时间没有变化。您是否尝试过使用元组(如果可以的话)?在这个问题上,元组和字典结合起来对我来说要快得多。只是胡乱猜测…不知道你的意思-你建议我在哪里使用元组?我想保留两个不同的
Tuple
hashset来保存数据。然后基本上做你现在正在做的事情。我认为带元组的哈希集可能更快,但我还没有测试过。不管怎样,数组是按ID排序的吗?因为在这种情况下,你的“双重”和“扁平”想法是这类事情最好的速度智慧。然而,如果这是太多的工作,那么我的hashset建议可能也是。我想你指的是Hashtable。我现在试过了,它比使用字典慢。仍然优化对哈希表不利。您的计时代码是在两个字典上运行的,每个字典大约有500.000个条目吗?我想复制5s运行。有了500.000个项目和两个字典,您的代码在我的机器上是“即时”的。如果稀疏数组是聚集的(即索引值往往很接近,但可能从任意位置开始),另一种可能性是存储“平面”数组和起始索引。然后,可以在两个索引集的交点上点阵列。根据您的问题领域,可能会有索引转换产生这种类型的聚类(例如,一个条目聚集在对角线附近的矩阵可以重新索引,以将对角线和接近对角线的元素彼此靠近。)谢谢。分类名单是act
Dict: 5.38s
Array: 1.87s
Dict: 4.70s
Array: 0.38s