【问题标题】:best data structure for storing large number of numeric fields存储大量数字字段的最佳数据结构
【发布时间】:2016-06-07 18:03:00
【问题描述】:

我正在使用一个类,比如 Widget,它具有大量的数字现实世界属性(例如,高度、长度、重量、成本等)。有不同类型的小部件(链轮、齿轮等),但每个小部件共享完全相同的属性(当然,小部件的值会有所不同,但它们都有重量、重量等)。我有 1,000 个每种类型的小部件(1,000 个齿轮、1,000 个链轮等)

我需要对这些属性执行大量计算(比如计算 1000 个不同小部件的属性的加权平均值)。对于加权平均值,我对每种小部件类型都有不同的权重(即,我可能更关心链轮的长度而不是齿轮的长度)。

现在,我将所有属性存储在每个小部件内的 Dictionary 中(小部件有一个枚举来指定它们的类型:cog、sprocket 等)。然后我有一些计算器类将每个属性的权重存储为 Dictionary>。要计算每个小部件的加权平均值,我只需遍历其属性字典键,例如:

double weightedAvg = 0.0;
foreach (string attibuteName in widget.Attributes.Keys)
{
    double attributeValue = widget.Attributes[attributeName];
    double attributeWeight = calculator.Weights[widget.Type][attributeName];
    weightedAvg += (attributeValue * attributeWeight);
}

所以这工作正常,可读性强且易于维护,但对于基于某些分析的 1000 多个小部件来说非常慢。我的属性名称世界是已知的,并且在应用程序的生命周期内不会改变,所以我想知道有哪些更好的选择。我能想到的几个:

1) 将属性值和权重存储在双 [] 中。我认为这可能是最有效的选择,但是我需要确保数组始终以正确的顺序存储在小部件和计算器之间。这也将数据与元数据解耦,因此我需要将一个数组 (?) 存储在某个位置,该数组将属性名称和索引映射为属性值和权重的双 []。

2) 将属性值和权重存储在不可变结构中。我喜欢这个选项,因为我不必担心排序并且数据是“自我记录”的。但是有没有一种简单的方法可以在代码中循环这些属性?我有近 100 个属性,所以我不想硬编码代码中的所有属性。我可以使用反射,但我担心这会导致更大的惩罚,因为我循环了这么多小部件并且必须对每个小部件使用反射。

还有其他选择吗?

【问题讨论】:

  • 当你说你需要循环超过100个属性时,你的意思是每个类都有100个属性吗?或者你的意思是你必须在一个属性(例如重量)上循环超过 100 个类的实例?我认为每个更具体的类都将继承的基类和为单个小部件进行计算的单个方法是合适的。
  • 您可以将所有内容存储在一个矩阵中(请参阅numerics.mathdotnet.com 以获取一个非常棒的免费 nuget 包),然后您可以实现直接获取和设置矩阵值的属性。这样您就可以轻松访问,但在计算计算方面表现出色。
  • @ChrisDunaway,现在我只有 1 个小部件类,小部件的类型只是存储为类的属性(枚举值)。我可以将小部件类型设置为不同的子类,但我认为这不会解决我的问题。无论如何,要回答您的问题,Widget 类有 100 个属性。因此,要计算任何一个小部件的加权平均值,我需要执行 100 次乘法和加法。但我也在对 1000 个不同的小部件实例执行此加权平均计算。
  • 它们的属性列是相同的还是不同的?
  • @MeirionHughes,感谢您的建议。我会为每个小部件实例创建一个新矩阵吗?还是您建议我创建一个“全局”属性矩阵(带有 num_widget 行和 num_attribute 列),然后在每个小部件实例的 get/set 方法中使用逻辑来直接访问矩阵?第二种方法听起来很有趣,但我不确定将全局矩阵存储在哪里,以及自定义 getter/setter 方法的开销是否会比当前 Dictionary 版本的开销更好。

标签: c# data-structures


【解决方案1】:

三种可能性立即浮现在脑海中。第一个,我认为你太容易拒绝了,是在你的班级中有单独的领域。也就是说,名为heightlengthweightcost 等的单个double 值是正确的,它会需要更多代码来进行计算,但你不会有间接性字典查找。

第二个是放弃字典,转而使用数组。所以不是Dictionary<string, double>,而是double[]。再说一次,我认为你拒绝的太快了。您可以轻松地将字符串字典键替换为枚举。所以你有:

enum WidgetProperty
{
    First = 0,
    Height = 0,
    Length = 1,
    Weight = 2,
    Cost = 3,
    ...
    Last = 100
}

鉴于此和double 的数组,您可以轻松浏览每个实例的所有值:

for (int i = (int)WidgetProperty.First; i < (int)WidgetProperty.Last; ++i)
{
    double attributeValue = widget.Attributes[i];
    double attributeWeight = calculator.Weights[widget.Type][i];
    weightedAvg += (attributeValue * attributeWeight);
}

直接数组访问比通过字符串访问字典要快得多。

最后,您可以稍微优化一下您的字典访问。与其对键执行 foreach 然后进行字典查找,不如对字典本身执行 foreach

foreach (KeyValuePair<string, double> kvp in widget.Attributes)
{
    double attributeValue = kvp.Value;
    double attributeWeight = calculator.Weights[widget.Type][kvp.Key];
    weightedAvg += (attributeValue * attributeWeight);
}

【讨论】:

  • 谢谢吉姆,这个答案很棒。我什至没有考虑过你可以这样循环枚举。我曾考虑用枚举替换字典中的字符串键,但使用枚举 + 数组似乎更好。
【解决方案2】:

要在不循环或反射的情况下计算加权平均值,一种方法是计算各个属性的加权平均值并将它们存储在某个位置。这应该在您创建小部件实例时发生。以下是一个示例代码,需要根据您的需要进行修改。 此外,为了进一步处理小部件本身,您可以使用数据并行性。在这个帖子中查看我的其他回复。

public enum WidgetType { }

public class Claculator { }

public class WeightStore
{
    static Dictionary<int, double> widgetWeightedAvg = new Dictionary<int, double>();
    public static void AttWeightedAvgAvailable(double attwightedAvg, int widgetid)
    {
        if (widgetWeightedAvg.Keys.Contains(widgetid))
            widgetWeightedAvg[widgetid] += attwightedAvg;
        else
            widgetWeightedAvg[widgetid] = attwightedAvg;
    }
}

public class WidgetAttribute
{
    public string Name { get; }
    public double Value { get; }
    public WidgetAttribute(string name, double value, WidgetType type, int widgetId)
    {
        Name = name;
        Value = value;
        double attWeight = Calculator.Weights[type][name];
        WeightStore.AttWeightedAvgAvailable(Value*attWeight, widgetId);
    }
}

public class CogWdiget
{
    public int Id { get; }
    public WidgetAttribute height { get; set; }
    public WidgetAttribute wight { get; set; }
}

public class Client
{
    public void BuildCogWidgets()
    {
        CogWdiget widget = new CogWdiget();
        widget.Id = 1;
        widget.height = new WidgetAttribute("height", 12.22, 1);
    }
}

【讨论】:

  • 所以您建议将加权值缓存一次(在对象创建时)?谢谢,这是一个有用的想法,我没有想过。我不确定它是否适用于我的情况,因为有很多很多不同的计算器,并且大多数可能只能在小部件的子集上调用(我不会提前知道),所以我最终会缓存很多可能永远不会被使用的数据。但我会考虑的。
【解决方案3】:

与数据规范化一样,选择规范化级别决定了性能的很大一部分。看起来您必须从当前模型转换为另一个模型或混合模型。

如果您不使用 C# 端而是使用数据库来处理此问题,则可能会为您的方案提供更好的性能。然后,您将获得索引的好处,除了想要的结果之外没有数据传输,再加上已经花费了 100000 工时在性能优化上。

【讨论】:

  • OP 没有提到任何关于数据库的内容,这似乎更像是评论而不是 asnwer。
【解决方案4】:

使用 .net 4 及更高版本支持的数据并行。

https://msdn.microsoft.com/en-us/library/dd537608(v=vs.110).aspx

以上链接摘录

当并行循环运行时,TPL 对数据源进行分区,以便循环可以同时对多个部分进行操作。在幕后,任务计划程序根据系统资源和工作负载对任务进行分区。如果工作负载变得不平衡,调度程序会在可能的情况下在多个线程和处理器之间重新分配工作

【讨论】:

    猜你喜欢
    • 2021-01-12
    • 1970-01-01
    • 2011-10-09
    • 1970-01-01
    • 2011-11-26
    • 1970-01-01
    • 1970-01-01
    • 2010-09-25
    • 1970-01-01
    相关资源
    最近更新 更多