ExpandoObject和Dictionary在性能方面有何区别?

10

一个相当简单的问题。我正在处理一个需要动态存储和检索属性值的项目,这些值将被写入现在和以后多次读取。检索速度是最重要的,每纳秒都很重要。

通常,我会使用Dictionary实现这个功能,但随着C# 4和ExpandoObject的出现,我想也许有更好的方法?是否有任何人有经验可以分享?我看到在其他帖子中,它没有使用字典实现,这让我很好奇它是否更快或更慢?

让我试着用一些伪代码来说明:

// In the main loop
var context = new Context();
context["MyKey"] = 123;
context["MyOtherKey"] = "CODE";
context["MyList"] = new List<int>() { 1, 12, 14 };

foreach(var handler in handlers) {
    handler.DoStuff(context);
}

-

// "Handlers"
class MyFirstHandler {
     void DoStuff(Context context) {
          if (context["MyKey"] > 100)
               context["NewKey"] = "CODE2";
     }
}

class MySecondHandler {
     void DoStuff(Context context) {
          if (context["MyOtherKey"] == "CODE")
             context["MyList"].Add(25); // Remember, it's only Pseudo-code..
     }
}

希望你能理解我试图做的事情。

在这里我完全开放接受其他建议。我一直在考虑将Context类静态类型化(即实际具有MyKey属性、MyOtherKey属性等),尽管这可能是可能的,但它会极大地妨碍我们的生产力。


看起来你有很多字符串常量,这是否意味着你提前有一个字典键列表?如果是这样,只需使用普通类即可避免哈希函数的开销。 - Juliet
是的,实际上我只使用字符串常量来简化伪代码。在“当前”的真实世界示例中,我正在使用更快的键。 - CodingInsomnia
这里有一个简单的比较。如果你可以忽略构建成本,查找应该表现类似。 - nawfal
3个回答

11

检索速度是最重要的,每纳秒都很关键。

任何与动态(dynamic)有关的内容可能并不是您正在寻找的内容...

别误会,它已经进行了相当大量的优化-但是,如果您基本上只想要一个字符串到字符串的字典查找,请使用字典。

或者,如果您有一些有限的键,您是否考虑过只使用数组,将枚举或一些 int 常量作为键?


谢谢,这差不多是我预期的(但不是我希望的...)我认为使用简单数组的问题与静态类型化Context类的问题基本相同,即我需要一个中央位置来了解所有可能的键。虽然可以做到,但我更喜欢采用更灵活的方法。 - CodingInsomnia
1
@CodingInsomnia:如果你不想有一个有限的键集,那么Dictionary<,>绝对是最好的选择 - 但是不要在使用它们的代码中将键硬编码为字符串文字...最好使用字符串常量来避免拼写错误。 - Jon Skeet
是的,绝对没错。实际上我不会使用字符串作为键,那只是为了简化伪代码。 - CodingInsomnia
我知道这是一个比较老的帖子,但如果我有一个json数据,每次通过API更改时都会加载它。这个数据代表了一个UDP数据报,可能有许多变量及其类型和可能的值。我想实时创建一个数据报对象,其中包含头部负载信息。但我在想,如果每次都必须从机器解析字节数组并将其发送回高级API作为JSON,然后再获取JSON并解析回字节,然后再将其发送到机器,性能会如何?每个这样的循环可以长达毫秒。什么才是更好的选择?动态对象? - kuldeep
通过 .net core 3.1,一种方法是基于动态 JSON 对数据进行序列化和反序列化。对于头部和有效载荷都是如此。我有点担心如果有效载荷在每个周期中都被序列化和反序列化,性能会受到影响。 - kuldeep
1
@kuldeep:我建议您提出一个新问题,并尽可能提供更多的上下文,最好附带完整的示例。 - Jon Skeet

3

如果字符串列表事先已知,您可以使用IL Emit基于搜索字符串中的字符创建分支树,并解析为数组索引。这应该能够提供相当快的查找速度。

我在学习IL Emit时为了好玩和练习实现了类似的东西。它基于我尝试过的有限测试用例工作,但你一定要使它更加健壮,并为生产代码创建适当的单元测试。我发布了原始代码(有点长);您需要更改一些内容以适应您的特定情况,但核心逻辑已经存在。我没有包括EmitLdc帮助函数(有很多重载),但它只是一个将任意常量加载到堆栈的函数。您可以直接使用Ldstr和Ldc_I4调用来替换发出字符串和数字类型的调用。

    protected void GenerateNestedStringSearch<T>(ILGenerator gen, T[] values, Func<T, string> getName, Action<ILGenerator, T> loadValue)
    {
        //We'll jump here if no match found
        Label notFound = gen.DefineLabel();

        //Try to match the string
        GenerateNestedStringSearch(gen, notFound, values, getName, loadValue, 0);

        //Nothing found, so don't need string anymore
        gen.MarkLabel(notFound);
        gen.Emit(OpCodes.Pop);

        //Throw ArgumentOutOfRangeException to indicate not found
        gen.EmitLdc("name");
        gen.EmitLdc("Binding does not contain a tag with the specified name: ");
        gen.Emit(OpCodes.Ldarg_0);
        gen.Emit(OpCodes.Call, typeof(String).GetMethod("Concat",
                                                        BindingFlags.Static | BindingFlags.Public,
                                                        null,
                                                        new[] { typeof(string), typeof(string) },
                                                        null));
        gen.Emit(OpCodes.Newobj,
                 typeof(ArgumentOutOfRangeException).GetConstructor(new[] { typeof(string), typeof(string) }));
        gen.Emit(OpCodes.Throw);
    }

    protected void GenerateNestedStringSearch<T>(ILGenerator gen, Label notFound, T[] values, Func<T, string> getName, Action<ILGenerator, T> loadValue, int charIndex)
    {
        //Load the character from the candidate string for comparison
        gen.Emit(OpCodes.Dup);
        gen.EmitLdc(charIndex);
        gen.Emit(OpCodes.Ldelem_U2);

        //Group possible strings by their character at this index
        //We ignore strings that are too short
        var strings = values.Select(getName).ToArray();
        var stringsByChar =
            from x in strings
            where charIndex < x.Length
            group x by x[charIndex]
                into g
                select new { FirstChar = g.Key, Strings = g };

        foreach (var grouped in stringsByChar)
        {
            //Compare source character to group character and jump ahead if it doesn't match
            Label charNotMatch = gen.DefineLabel();
            gen.Emit(OpCodes.Dup);
            gen.EmitLdc(grouped.FirstChar);
            gen.Emit(OpCodes.Bne_Un, charNotMatch);

            //If there is only one string in this group, we've found our match
            int count = grouped.Strings.Count();
            Debug.Assert(count > 0);
            if (count == 1)
            {
                //Don't need the source character or string anymore
                gen.Emit(OpCodes.Pop);
                gen.Emit(OpCodes.Pop);

                //Return the value for this name
                int index = Array.FindIndex(strings, s => s == grouped.Strings.First());
                loadValue(gen, values[index]);
                gen.Emit(OpCodes.Ret);
            }
            else
            {
                //Don't need character anymore
                gen.Emit(OpCodes.Pop);

                //If there is a string that ends at this character
                string endString = grouped.Strings.FirstOrDefault(s => s.Length == (charIndex + 1));
                if (endString != null)
                {
                    //Get string length
                    gen.Emit(OpCodes.Dup);
                    gen.Emit(OpCodes.Call, typeof(char[]).GetProperty("Length").GetGetMethod());

                    //If string length matches ending string
                    gen.EmitLdc(endString.Length);
                    Label keepSearching = gen.DefineLabel();
                    gen.Emit(OpCodes.Bne_Un, keepSearching);

                    //Don't need the source string anymore
                    gen.Emit(OpCodes.Pop);

                    //Create an UnboundTag for this index
                    int index = Array.FindIndex(strings, s => s == endString);
                    loadValue(gen, values[index]);
                    gen.Emit(OpCodes.Ret);

                    //String length didn't match
                    gen.MarkLabel(keepSearching);
                }

                //Need to consider strings starting with next character
                var nextValues = from s in grouped.Strings
                                 join v in values on s equals getName(v) 
                                 select v;

                GenerateNestedStringSearch(gen, notFound, nextValues.ToArray(),
                    getName, loadValue, charIndex + 1);
            }

            //This character didn't match, so consider next character
            gen.MarkLabel(charNotMatch);
        }

        //We don't need the character anymore
        gen.Emit(OpCodes.Pop);

        //No string match, so jump to Not Found at end of check
        gen.Emit(OpCodes.Br, notFound);
    }

编辑:我刚意识到你实际上并没有使用字符串键,所以这可能不适用于你的情况。只要您有能力事先收集所有所需的键,就可以使用类似的技术进行其他查找。如果有人发现有用,我会保留这个技巧。


由于他正在使用C# 4.0,因此他可以使用表达式树来构建语句。我之前写过一篇关于它的文章(语句在其中底部): http://translate.google.com/translate?js=y&prev=_t&hl=sv&ie=UTF-8&layout=1&eotf=1&u=http%3A%2F%2Fweblogs.asp.net%2Fmikaelsoderstrom%2Farchive%2F2009%2F09%2F27%2Ff-246-rst-229-expression-trees.aspx&sl=sv&tl=en我用瑞典语写的,这就是为什么我有一个指向Google翻译的链接。 :) - Mikael Söderström
3
从英语翻译成中文。仅返回已翻译的文本:+1 对于一个非常有创意的答案 - 可能无法使用,但仍然..! - CodingInsomnia

2

第一次调用必须如此快吗?由于调用站点缓存,动态对象创建的表达式树(包括您添加到其中的方法)在编译后被缓存,并且在再次使用时将返回。

使用ExpandoObject应该可以工作,但如果您确实需要获得最佳性能,也许应该使用自定义类型。


听起来非常有趣。第一次调用并不是非常关键,因为每个属性通常会被调用很多次,所以如果从一个字典中检索值比较快,那么这可能是值得的。我想我只需要尝试几种不同的方法... - CodingInsomnia
最好的方法就是填充大量的虚拟数据,然后看哪种方法最快。 :) - Mikael Söderström

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接