dotnet ConcurrentDictionary 的 GetOrAdd 性能比 TryGetValue 加 TryAdd 低
我在 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 方法的性能更好,同时申请的对象也更少
Method |
Mean |
Error |
StdDev |
Gen 0 |
Gen 1 |
Gen 2 |
Allocated |
---|---|---|---|---|---|---|---|
Create |
22.19 ns |
0.154 ns |
0.144 ns |
- |
- |
- |
- |
Create2 |
37.22 ns |
0.337 ns |
0.315 ns |
0.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
Method |
Mean |
Error |
StdDev |
Median |
---|---|---|---|---|
GetOrAddExistWithClosed |
1.702 μs |
0.0339 μs |
0.0772 μs |
1.659 μs |
GetOrAddExistWithValue |
1.586 μs |
0.0460 μs |
0.1335 μs |
1.518 μs |
GetOrAddNotExistWithClosed |
1.422 μs |
0.0181 μs |
0.0141 μs |
1.417 μs |
GetOrAddNotExistWithValue |
1.591 μs |
0.0665 μs |
0.1940 μs |
1.529 μs |
GetOrAddExistWithoutClosed |
1.986 μs |
0.0204 μs |
0.0180 μs |
1.991 μs |
GetOrAddNotExistWithoutClosed |
2.054 μs |
0.0167 μs |
0.0130 μs |
2.057 μs |
TryGetValueExist |
1.149 μs |
0.0132 μs |
0.0117 μs |
1.144 μs |
TryGetValueNotExist |
1.281 μs |
0.0353 μs |
0.1019 μs |
1.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);
}
}
}
本文会经常更新,请阅读原文: https://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.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
- Java中ArrayList remove会遇到的坑
- Dagger2 入门解析
- Git 工作流的正确打开方式
- 如何从两个List中筛选出相同的值
- 【Windows编程】系列第八篇:创建通用对话框
- 使用dropwizard(3)-加入DI-dagger2
- 巧用shell生成数据库检查脚本 (74天)
- 【专业技术】OPENGL与EGL
- 在dropwizard中使用feign,使用hystrix
- 用python抓取淘宝评论
- 使用Dropwizard(2)-配置分类ConfiguredBundle
- Upgrade with the Gradle Wrapper, gradlew升级
- 使用swagger作为restful api的doc文档生成
- 面试机器学习、大数据岗位时遇到的各种问题
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 从零开始Kubernetes Operator
- TiKV源码解析系列文章(二十)Region Split源码解析
- scrapy爬虫框架和selenium的使用:对优惠券推荐网站数据LDA文本挖掘
- 单性状动物模型矩阵形式计算BLUP值
- 如何计算一般配合力和特殊配合力
- 【29期】Java集合框架 10 连问,你有被问过吗?
- 学徒数据挖掘之谁说生存分析一定要按照表达量中位值或者平均值分组呢?
- 软件质量的黄金准则
- 你应该了解的5种TypeScript设计模式
- Vue.js 教程:构建一个特斯拉汽车余电计算器
- 文件系统:隐匿在 Linux 背后的机制
- MySQL在Docker环境下运行基础
- Redis 秒杀实战
- Oracle 19c集群重装步骤详解
- 神经网络训练中回调函数的实用教程