简体   繁体   English

C#Dictionary性能:默认字符串Comparer的GetHashCode()违反指南分配内存,从而破坏性能?

[英]C# Dictionary Performance: Default string Comparer's GetHashCode() allocates memory in violation of guidelines, thus wrecking performance?

There is an established guideline that getting a hashcode should not allocate memory because this will negatively impact hash table lookups by invoking the garbage collector. 一个既定的准则 ,即获取哈希码不应该分配内存,因为这将通过调用垃圾收集器对哈希表查找产生负面影响。

Yet this exact failing is what I see what I profile my application which uses a System.Collections.Generic.Dictionary 然而,这种确切的失败是我看到我使用System.Collections.Generic.Dictionary我的应用程序的概况

Way deep down in a very tight loop I find the following in my profiler results: 在非常紧凑的循环中,我在我的分析器结果中找到以下内容:

  • [3.47%] TryGetValue(TKey, TValue&) (...Dictionary) [3.47%] TryGetValue(TKey,TValue&)(...字典)
    • [3.47%] FindEntry(TKey) (...Dictionary) [3.47%] FindEntry(TKey)(...字典)
      • [3.47%] GetHashCode(string) (System.CultureAwareComparer) [3.47%] GetHashCode(string)(System.CultureAwareComparer)
        • [3.46%] GetHashCodeOfString(String, CompareOptions) (System.Globalization.CompareInfo) [3.46%] GetHashCodeOfString(String,CompareOptions)(System.Globalization.CompareInfo)
          • [3.39%] [Garbage Collection] [3.39%] [垃圾收集]
          • [0.01%] [Thread Suspendended] [0.01%] [线程悬空]

That's the whole sub-tree accounting from the profiler. 这是分析器的整个子树会计。

I'm not a seasoned expert in this specific sort of work, so I could be reading these tea leaves incorrectly. 我不是这种特殊工作的经验丰富的专家,所以我可能会错误地阅读这些茶叶。 But it looks to me like GetHashCodeOfString "must be" allocating memory and inviting the garbage collector to interrupt my program in the middle of this loop I want REALLY TUNED AND TIGHT, and this is accounting for the staggering majority of the cost of this loop. 但它看起来像GetHashCodeOfString“必须”分配内存并邀请垃圾收集器在这个循环的中间中断我的程序我想要真正的调整和紧,这是占这个循环的惊人的大部分成本。

As an aside, here is an additional piece of evidence suggesting this code allocates memory 顺便说一句, 这里有一个额外的证据表明这个代码分配了内存

My next step will be to initialize the Dictionary with the ordinal comparer and re-run my tests. 我的下一步是使用序数比较器初始化Dictionary并重新运行我的测试。

But I want to know if there is existing wisdom out there around this issue. 但我想知道围绕这个问题是否存在现有的智慧。 It seems like dictionaries with string keys are common, and the costs of such a common thing may be well explored. 看起来像带有字符串键的字典很常见,并且可以很好地探索这种常见事物的成本。 I found the following analysis, but it focuses on the actual comparison as the cause for woe, and not the hash code method allocating memory. 我发现了以下分析,但它侧重于实际比较作为祸患的原因,而不是分配内存的哈希码方法。

Can anyone suggest the proper way to use a dictionary with string keys that avoids this problem? 任何人都可以建议使用字符串键的字典的正确方法,以避免这个问题?

Specific questions I have include: 我有的具体问题包括:

  • If I use the ordinal comparitor will the allocation go away? 如果我使用序数比较器,分配会消失吗?
  • If not, do I need to write my own comparitor, and will THAT make the allocation go away? 如果没有,我是否需要编写自己的比较器,这会使分配消失吗?
  • If I do make the comparitor go away, can I really expect a real improvement, as per the MSFT recommendation link I started with? 如果我确实让比较器消失了,根据我开始的MSFT推荐链接,我真的可以期待真正的改进吗?

EDIT: Crud, my bad, but this is not with the default comparer properties, we have it set to ignoreCase. 编辑:Crud,我的坏,但这不是默认的比较器属性,我们将它设置为ignoreCase。 Not sure if this impacts the results, but since ignoreCase would impact the equality, it must therefor have some impact on the hash. 不确定这是否会影响结果,但由于ignoreCase会影响相等性,因此必须对哈希产生一些影响。

UPDATE: Ran another test using the ordinal comparer (still with IgnoreCase), and recast the original results output to 100% cost = TryGetValue so it would be more apples to apples 更新:使用序数比较器(仍然使用IgnoreCase)进行另一个测试,并将原始结果输出重新设置为100%cost = TryGetValue,这样它就会更多苹果到苹果

Original: 原版的:

  • 100% TryGetValue 100%TryGetValue
    • 100% FindEntry 100%FindEntry
      • 99.5% CultureAwareComparer.GetHashCode 99.5%CultureAwareComparer.GetHashCode
        • 99.5% CompareInfo.GetHashCodeOfString 99.5%CompareInfo.GetHashCodeOfString
          • 95.86% [Garbage Collection] 95.86%[垃圾收集]
          • 3.31% [Thread Suspended] 3.31%[线程暂停]
      • 0.5% CultureAwareComparer.Equals 0.5%CultureAwareComparer.Equals
        • 0.5% Compare 0.5%比较
          • 0.5% [garbage collection] 0.5%[垃圾收集]

Ordinal: 序:

  • 100% TryGetValue 100%TryGetValue
    • 100% FindEntry 100%FindEntry
      • 47.22% CultureAwareComparer.Equals 47.22%CultureAwareComparer.Equals
        • 47.22% [Garbage Collection] 47.22%[垃圾收集]

There also appeared to be a dramatic decrease in the overall time spend in TryGetValue. TryGetValue的总花费时间也显着减少。 I was not careful to make sure all else was equal, but this accounted for 46 seconds out of a 10 minute stress test in the first run, and in the orindal run it accounted for 252 milliseconds. 我不小心确保所有其他方面都相同,但在第一次运行的10分钟压力测试中这占了46秒,而在orindal运行中它占了252毫秒。 Consider that anecdotal, not an expected relative cost. 考虑一下轶事,而不是预期的相对成本。

It seems like the entire cost of the hash, which used to be 99+% of the cost, is now so "free" that it fails to even appear in the profiler, which I think is running in sampling mode. 看起来哈希的全部成本(曾经是成本的99%以上)现在是如此“自由”,以至于它甚至无法出现在探查器中,我认为它在采样模式下运行。

I guess this seconds the word on the street that you should use ordinal comparison. 我猜这个街道上的这个词你应该使用顺序比较。

I still can't PROVE to myself why the GC cost is contributing so heavily to the first profile result, but from the comments below I suppose I have to believe it does NOT allocate managed heap memory, but that because it's slow, it tends to be the function that is "randomly" GCed by other activities on other threads, as this process is indeed using server mode gc. 我仍然无法向自己说明为什么GC成本对第一个配置文件结果的贡献如此之大,但是从下面的评论中我想我必须相信它不会分配托管堆内存,但是因为它很慢,所以它倾向于是由其他线程上的其他活动“随机”GC的函数,因为此进程确实使用服务器模式gc。

Maybe this indicates that this tight loop tends to be concurrent with allocation-happy code someplace else. 也许这表明这个紧密循环往往与其他地方的分配快乐代码并发。

By default, when you use string keys, string.GetHashCode() is used. 默认情况下,使用string键时,使用string.GetHashCode() This method doesn't allocate any memory on the heap, and should be pretty fast. 此方法不会在堆上分配任何内存,并且应该非常快。

But since you're using ignore case, CultureAwareComparer.GetHashCode() is used instead. 但由于您使用的是ignore case,因此使用CultureAwareComparer.GetHashCode() That method calls (as can be seen from your profile results) CompareInfo.GetHashCodeOfString() , which in turn calls the unmanaged function InternalGetGlobalizedHashCode() . 该方法调用(从您的配置文件结果中可以看出) CompareInfo.GetHashCodeOfString() ,后者又调用非托管函数InternalGetGlobalizedHashCode() Neither of the two managed methods makes any heap allocations (as you can see if you look at them in a decompiler). 两个托管方法都没有进行任何堆分配(如果你在反编译器中查看它们就可以看到)。 I can't say what InternalGetGlobalizedHashCode() does, but since it is unmanaged, I doubt it makes any allocations on the managed heap. 我不能说InternalGetGlobalizedHashCode()做了什么,但由于它是非托管的,我怀疑它是否在托管堆上进行任何分配。 In any case, it has to be quite a lot more complex than the default hash code computation, especially since it is culture-aware and has to keep in mind issues like the Turkish İ . 无论如何,它必须比默认的哈希码计算复杂得多,特别是因为它具有文化意识并且必须记住土耳其语等问题。

What this means is that you probably have some other code that allocates memory on the heap, which causes the garbage collection. 这意味着您可能有一些其他代码在堆上分配内存,这会导致垃圾回收。

And if you are going for maximum performance, you should avoid “ignore case”, and especially its culture-aware variants. 如果你想要获得最大的性能,你应该避免“忽视案例”,特别是它的文化意识变种。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM