Skip to content

搞英语 → 看世界

翻译英文优质信息和名人推特

Menu
  • 首页
  • 作者列表
  • 独立博客
  • 专业媒体
  • 名人推特
  • 邮件列表
  • 关于本站
Menu

简化迭代:探索 C++20 中的键和值

Posted on 2025-04-21

在软件中,我们经常使用键值数据结构,其中每个键都是唯一的并映射到特定值。常见的示例包括 Python 中的字典、Java 中的哈希映射和 JavaScript 中的对象。如果将数组与键值数据结构结合起来,就可以表示大多数数据。

在 C++ 中,我们有两个标准的键值数据结构:std::map 和 std::unordered_map。通常,std::map被实现为具有排序键的树(例如,红黑树),为查找、插入和删除提供对数时间复杂度O(log n),并且以排序顺序维护键。 std::unordered_map 通常实现为具有未排序键的哈希表,为查找、插入和删除提供平均情况常数时间复杂度 O(1)。在 std::unordered_map 中,哈希表使用哈希函数将键映射到存储桶数组中的索引。每个桶本质上是一个可以容纳多个键值对的容器,通常实现为链表。当对键进行哈希处理时,哈希函数会计算与存储桶相对应的索引。如果多个键哈希到同一个存储桶(冲突),它们将存储在该存储桶的链表中。

很多时候,我们只需要查看键,或者查看值。 C++20 标准通过引入范围(std::ranges::views::keys 和 std::ranges::views::values)使这一点变得方便。让我们考虑使用“现代”函数风格的两个函数。第一个函数对值求和,下一个函数计算有多少个键(假设它们是字符串)以给定前缀开头。

模板<类型名称map_type > 自动求和值( const map_type & map ) { 自动值=地图| std ::范围::视图::值; 返回std :: accumulate ( values.begin ( ) , values.end ( ) , 类型名map_type :: mapped_type { } ) ; }  模板<类型名称map_type > size_t count_keys_with_prefix ( const map_type & map , std :: string_view 前缀) { 自动键=地图| std ::范围::视图::键; 返回std :: ranges :: count_if (键, [前缀] ( std :: string_view key ) { 返回键。开始于(前缀) ; } ) ; }
该代码定义了两个模板函数来处理类似地图的容器。 sum_values 函数采用任何类型的映射,并通过使用 std::ranges::views::values 提取它们来计算其值的总和,然后应用 std::accumulate 将它们相加,从映射值类型的默认构造值开始。 count_keys_with_prefix 函数通过使用 std::ranges::views::keys 提取键并使用带有 lambda 的 std::ranges::count_if 来计算映射中以给定前缀开头的键数量,该 lambda 通过starts_with 检查每个键是否以指定前缀开头。这两个函数都利用 C++20 的范围库来对映射的键和值进行简洁且富有表现力的操作。
如果您不渴望函数式风格,您可以使用循环……就像这样……
模板<类型名称map_type >自动sum_values_daniel ( map_type & map ) { 类型名map_type :: mapped_type sum { } ; for ( const auto & value :地图| std ::范围::视图::值) {  总和+ =值; } 返回总和; }  模板<类型名称map_type > size_t count_keys_with_prefix_daniel ( const map_type & map , std :: string_view 前缀) { size_t计数= 0 ; for ( const auto & key :地图| std ::范围::视图::键) { if ( key .starts_with ( prefix ) ) { + +计数; } } 返回计数; }
我个人认为循环比 lambda 函数更具可读性,但可能值得对两者进行测试,看看哪一个提供最佳性能。

当您没有 C++20 时,您可以使用更传统的风格编写相同的函数:

模板<类型名称map_type > 类型名map_type :: mapped_type sum_values_cpp11 ( const map_type & map ) { 类型名map_type :: mapped_type sum = 类型名map_type :: mapped_type ( ) ; for ( typename map_type :: const_iterator it = map . begin ( ) ;  它! =地图.结尾( ) ; + +它) {  sum + = it - >第二个; } 返回总和; }  模板<类型名称map_type > size_t count_keys_with_prefix_cpp11 ( const map_type & map , const std ::字符串和前缀) { size_t计数= 0 ; for ( typename map_type :: const_iterator it = map . begin ( ) ;  它! =地图.结尾( ) ; + +它) { const std :: string & key = it - >第一个; if ( key . size ( ) > = prefix . size ( ) & &钥匙。比较( 0 ,前缀。大小( ) ,前缀) == 0 ) { + +计数; } } 返回计数; }
在 C++20 之前,没有专用的starts_with函数来直接检查一个字符串是否是另一个字符串的前缀,需要更详细的方法,例如compare。当然,您可以将旧式代码与“starts_with”方法结合起来。我认为添加像“starts_with”这样的函数是最近 C++ 标准的一个被低估的好处。小细节可能具有影响力。

让我们比较一下性能。我生成了一个大型键值映射,其中包含一千个键(作为字符串)以及相应的整数值。我所有的键都以“key_”开头,我要求我的函数计算有多少个键以“key_”开头(应该是全部)。我同时使用 std::map 和 std::unordered_map 数据源。我记录检查每个键需要花费多少纳秒。我使用两台机器,一台配备 Intel Ice Lake 处理器,运行频率为 3.2 GHz,使用 GCC 12 作为编译器,另一台配备 Apple M2 处理器,运行频率为 3.5 GHz,使用 LLVM 15 作为编译器。它们不使用相同的指令集,因此我们无法直接比较退役的指令数量:我们预计像 Apple M2 这样基于 ARM 的系统会使用更多指令。

使用 std::map 我得到以下计时和指令计数:

功能 苹果M2/LLVM15 英特尔 Ice Lake/GCC12
C++20 函数式 2.2纳秒 5.5纳秒
C++20 循环 2.0纳秒 5.5纳秒
C++11 4.0纳秒 5.5纳秒
C++11 与starts_with 2.0纳秒 5.5纳秒
功能 苹果M2/LLVM15 英特尔 Ice Lake/GCC12
C++20 函数式 58条指令 29 条指令
C++20 循环 41 条指令 29 条指令
C++11 58条指令 30条指令
C++11 与starts_with 43条指令 30条指令

使用 std::unordered_map,我得到以下计时和指令计数:

功能 苹果M2/LLVM15 英特尔 Ice Lake/GCC12
C++20 函数式 1.1纳秒 3.8纳秒
C++20 循环 0.9纳秒 5.4纳秒
C++11 0.9纳秒 5.4纳秒
C++11 与starts_with 0.9纳秒 5.4纳秒
功能 苹果M2/LLVM15 英特尔 Ice Lake/GCC12
C++20 函数式 35条指令 10 条说明
C++20 循环 30条指令 13条说明
C++11 26条指令 14条指令
C++11 与starts_with 32条指令 14条指令

我们发现迭代 std::map 实例比迭代 std::unordered_map 实例慢。

Apple M2/LLVM15 和 Intel Ice Lake/GCC12 系统之间的性能比较揭示了 C++ 代码执行方面的显着差异。虽然函数式方法(例如,C++20 范围)在 GCC 12 下的性能比传统循环高出 40%,但使用starts_with 进行字符串比较可将 Apple/LLVM15 上 std::map 的运行时间大幅缩短一半。

尽管适合缓存的输入大小较小,但英特尔的迭代性能不受计算限制,每周期可实现 2 条指令 (IPC),而苹果的 IPC 超过 4 条,凸显了其在优化数据访问模式方面的架构优势。

我的源代码可用。

原文: https://lemire.me/blog/2025/04/20/iterating-through-keys-and-values-in-c-with-c20-code/

本站文章系自动翻译,站长会周期检查,如果有不当内容,请点此留言,非常感谢。
  • Abhinav
  • Abigail Pain
  • Adam Fortuna
  • Alberto Gallego
  • Alex Wlchan
  • Answer.AI
  • Arne Bahlo
  • Ben Carlson
  • Ben Kuhn
  • Bert Hubert
  • Bits about Money
  • Brian Krebs
  • ByteByteGo
  • Chip Huyen
  • Chips and Cheese
  • Christopher Butler
  • Colin Percival
  • Cool Infographics
  • Dan Sinker
  • David Walsh
  • Dmitry Dolzhenko
  • Dustin Curtis
  • Elad Gil
  • Ellie Huxtable
  • Ethan Marcotte
  • Exponential View
  • FAIL Blog
  • Founder Weekly
  • Geoffrey Huntley
  • Geoffrey Litt
  • Greg Mankiw
  • Henrique Dias
  • Hypercritical
  • IEEE Spectrum
  • Investment Talk
  • Jaz
  • Jeff Geerling
  • Jonas Hietala
  • Josh Comeau
  • Lenny Rachitsky
  • Liz Danzico
  • Lou Plummer
  • Luke Wroblewski
  • Matt Baer
  • Matt Stoller
  • Matthias Endler
  • Mert Bulan
  • Mostly metrics
  • News Letter
  • NextDraft
  • Non_Interactive
  • Not Boring
  • One Useful Thing
  • Phil Eaton
  • Product Market Fit
  • Readwise
  • ReedyBear
  • Robert Heaton
  • Ruben Schade
  • Sage Economics
  • Sam Altman
  • Sam Rose
  • selfh.st
  • Shtetl-Optimized
  • Simon schreibt
  • Slashdot
  • Small Good Things
  • Taylor Troesh
  • Telegram Blog
  • The Macro Compass
  • The Pomp Letter
  • thesephist
  • Thinking Deep & Wide
  • Tim Kellogg
  • Understanding AI
  • 英文媒体
  • 英文推特
  • 英文独立博客
©2025 搞英语 → 看世界 | Design: Newspaperly WordPress Theme