Skip to content

dotnet ConcurrentDictionary 的 GetOrAdd 性能比 TryGetValue 加 TryAdd 低

Updated: at 12:43,Created: at 03:00

我在 Office 的 Open-XML-SDK 库里面找到有代码线程不安全,代码里面使用了 TryGetValue 加 TryAdd 的方法添加对象,而线程安全的方法是通过 GetOrAdd 方法。不过在小伙伴的评论我找到了 GetOrAdd 性能其实在有闭包的时候不如使用 TryGetValue 加 TryAdd 调用这两个方法,但是 GetOrAdd 的优势在于能做到只创建一次对象

Avoid multi-thread creates ElementMetadata object by lindexi · Pull Request #758 · OfficeDev/Open-XML-SDK 我找到了 OpenXML SDK 的代码存在线程不安全,代码如下

public static ElementMetadata Create(OpenXmlElement element)
{
var type = element.GetType();
// Use TryGetValue first for the common case of already existing types to limit number of allocations
if (_lookup.TryGetValue(type, out var result))
{
return result;
}
// 假设有两个线程进来,此时两个线程都判断 TryGetValue 不存在,于是就会使用 CreateInternal 创建对象
var metadata = CreateInternal(element);
_lookup.TryAdd(type, metadata);
return metadata;
}

也就是调用 Create 多线程调用将会创建多个不同的实例,如果修改为 GetOrAdd 方法,那么只会创建一个对象实例

但是如果在对象创建的时间可以忽略的前提下,如 CreateInternal 方法的耗时可以忽略,同时在 OpenXML 的这个业务里面,其实多创建对象是没有问题的,那么此时使用 TryGetValue 加上 TryAdd 的方法的性能会比使用 GetOrAdd 的性能高

这是我更改的方法,使用 GetOrAdd 可以做到只创建一个对象

public static ElementMetadata Create2(OpenXmlElement element)
{
var type = element.GetType();
// Use GetOrAdd first for the common case of already existing types to limit number of allocations
return _lookup.GetOrAdd(type, _ => CreateInternal(element));
}

此时做性能测试对比,性能测试的代码放在本文最后

可以看到使用 Create 方法的性能更好,同时申请的对象也更少

MethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
Create22.19 ns0.154 ns0.144 ns----
Create237.22 ns0.337 ns0.315 ns0.0210--88 B

为什么 Create2 方法会更慢,同时需要申请内存?原因是调用

每次使用 GetOrAdd 方法都需要创建一个 Lambda 表达式和传入参数,需要创建类,所以性能上不如原先代码

那么如果没有闭包呢?

接下来我测试了值存在和不存在等的比较,测试效果如下 GetOrAdd 需要传入一个 Lambda 表达式,这个表达式需要传入一个 element 变量,这将需要创建一个闭包

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.264 (19062/3/20H1)
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.301
[Host] : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT
DefaultJob : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT
MethodMeanErrorStdDevMedian
GetOrAddExistWithClosed1.702 μs0.0339 μs0.0772 μs1.659 μs
GetOrAddExistWithValue1.586 μs0.0460 μs0.1335 μs1.518 μs
GetOrAddNotExistWithClosed1.422 μs0.0181 μs0.0141 μs1.417 μs
GetOrAddNotExistWithValue1.591 μs0.0665 μs0.1940 μs1.529 μs
GetOrAddExistWithoutClosed1.986 μs0.0204 μs0.0180 μs1.991 μs
GetOrAddNotExistWithoutClosed2.054 μs0.0167 μs0.0130 μs2.057 μs
TryGetValueExist1.149 μs0.0132 μs0.0117 μs1.144 μs
TryGetValueNotExist1.281 μs0.0353 μs0.1019 μs1.229 μs

这里的 TryGetValueNotExist 就是使用 TryGetValue 判断之后再使用 TryAdd 加回去。同时每个 Key 都是不存在的,代码如下

[Benchmark]
public object TryGetValueNotExist()
{
object o = null;
for (int i = 0; i < Count; i++)
{
if (_concurrentDictionary.TryGetValue(i, out var value))
{
}
else
{
o = GetObject();
_concurrentDictionary.TryAdd(i, o);
}
}
return o;
}

而 GetOrAddExistWithClosed 就是使用 GetOrAdd 方法,同时 Key 是存在的,也就是每次获取的都是存在的相同的值。而 Closed 表示闭包,也就是存在一次闭包的委托创建,代码如下

[Benchmark]
public object GetOrAddExistWithClosed()
{
object o = null;
for (int i = 0; i < Count; i++)
{
o = GetObject();
_concurrentDictionary.GetOrAdd(-1, _ => o);
}
return o;
}

在 GetOrAdd 还有重载的方法,可以传入需要的参数,也就是 GetOrAddExistWithValue 方法,此时没有传入闭包,而是传入参数

[Benchmark]
public object GetOrAddExistWithValue()
{
object o = GetObject();
for (int i = 0; i < Count; i++)
{
o = _concurrentDictionary.GetOrAdd(-1, (_, value) => value, o);
}
return o;
}

同时测试了不传入闭包,也就是使用类的方法,代码如下

[Benchmark]
public object GetOrAddExistWithoutClosed()
{
object o = null;
for (int i = 0; i < Count; i++)
{
o = _concurrentDictionary.GetOrAdd(-1, _ => GetObject());
}
return o;
}

上面是测试 _concurrentDictionary 存在值的,因为在初始化给了 -1 的值,也就是每次获取都是存在值的

如果每次都是 Key 不存在的,也测试了性能就是对应的 NotExist 方法

上面测试的代码放在 github 欢迎小伙伴访问

这是在 OpenXML 的性能测试代码

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using BenchmarkDotNet.Attributes;
using DocumentFormat.OpenXml.Framework.Metadata;
namespace DocumentFormat.OpenXml.Benchmarks
{
public class ElementMetadataTests
{
[GlobalSetup]
public void Setup()
{
_element = new AlternateContent();
}
private OpenXmlElement _element;
[BenchmarkCategory("ElementMetadataTests")]
[Benchmark]
public void Create()
{
_ = ElementMetadata.Create(_element);
}
[BenchmarkCategory("ElementMetadataTests")]
[Benchmark]
public void Create2()
{
_ = ElementMetadata.Create2(_element);
}
}
}

知识共享许可协议

原文链接: http://blog.lindexi.com/post/dotnet-ConcurrentDictionary-%E7%9A%84-GetOrAdd-%E6%80%A7%E8%83%BD%E6%AF%94-TryGetValue-%E5%8A%A0-TryAdd-%E4%BD%8E

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。 欢迎转载、使用、重新发布,但务必保留文章署名 林德熙 (包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我 联系