当前位置: 首页 > ai >正文

CppCon 2014 学习:EFFICIENCY PERFORMANCE ALGORITHMS DATA STRUCTURES

你这段内容主要讲的是“做更少的工作,避免浪费努力(Do Less Work By Not Wasting Effort)”,通过更高效的算法### 子串搜索(Sub-string Searching)

  • 最初: 最基础的算法是 O(n²),比如暴力搜索,每次都从头开始比较。
  • 改进: Knuth-Morris-Pratt (KMP) 算法,使用一个“跳表”,跳过不必要的匹配。
  • 更进一步: Boyer-Moore 算法,利用“针(needle)”的末尾信息跳过更多字符,极大减少比较次数。
    核心思想: 减少不必要的比较,做到“少做事,快完成”。

代码示例:避免浪费努力

1. std::vector 动态扩容带来的额外开销
std::vector<X> f(int n) {std::vector<X> result;for (int i = 0; i < n; ++i)result.push_back(X());return result;
}
  • 这里 result 未提前预分配空间,每次扩容都可能导致重新分配和复制,浪费时间。
    改进为:
std::vector<X> f(int n) {std::vector<X> result;result.reserve(n);  // 提前分配好容量,避免多次扩容for (int i = 0; i < n; ++i)result.push_back(X());return result;
}
2. 缓存查找优化
X *getX(std::string key, std::unordered_map<std::string, std::unique_ptr<X>> &cache) {if (cache[key])return cache[key].get();cache[key] = std::make_unique<X>();return cache[key].get();
}
  • 这里每次 cache[key] 都访问两次,且访问会触发哈希查找两次。
    改成:
X *getX(std::string key, std::unordered_map<std::string, std::unique_ptr<X>> &cache) {std::unique_ptr<X> &entry = cache[key];  // 只访问一次哈希表if (entry)return entry.get();entry = std::make_unique<X>();return entry.get();
}

总结

  • 做更少的工作 = 避免重复计算、避免不必要的操作
  • 避免浪费努力 = 提前规划,减少资源浪费(比如内存重分配,重复查找)
  • 这是一种性能优化的思维方式,贯穿算法设计与代码实现的各个层面。

“拒绝链表(Linked Lists)”,因为现代CPU和内存架构导致链表性能很差,别用链表了!”

重点解析:

现代CPU超快,但访问内存的速度却没那么快!

  • CPU频率高达 3GHz,约 1,000,000,000 个周期每秒
  • 多核心、多执行端口,理论执行指令数高达 36,000,000,000 条每秒
  • 但实际上大量时间都在等待内存数据(估计50%时间)

CPU缓存层级(cache hierarchy)访问时间(大致数值):

操作时间说明
CPU周期(3 GHz处理器)1 ns最快
L1缓存访问0.5 nsCPU内部一级缓存最快访问
分支错误预测惩罚5 ns跳转错误带来惩罚
L2缓存访问7 ns二级缓存,比L1慢14倍
互斥锁(Mutex)开锁/解锁25 ns线程同步成本
主内存访问100 ns远远慢于缓存,L1的200倍
Snappy压缩1K字节3,000 nsCPU计算操作开销
1Gbps网络发送1K字节10,000 ns网络传输
SSD随机读4K150,000 ns远比内存慢
内存顺序读1MB250,000 ns大块数据顺序读
数据中心内部往返延迟500,000 ns网络延迟
SSD顺序读1MB1,000,000 nsSSD比内存慢4倍
磁盘寻址10,000,000 ns远比SSD慢
磁盘顺序读1MB20,000,000 ns磁盘访问非常慢
跨大洲数据包往返延迟(加州到荷兰)150,000,000 ns极慢,150 ms级延迟

为什么说“拒绝链表”?

  • 链表访问不是顺序访问,缓存命中率极低。
    链表节点分散在内存不同位置,访问时大量缓存未命中,导致访问延迟高达几十倍甚至上百倍。
  • 现代CPU高速执行和深层缓存机制要求访问模式尽可能连续,链表这种跳跃式访问严重拖慢性能。
  • 数组(vector)或连续内存结构 代替链表,提升缓存利用率,减少内存访问等待。

结论:

  • 现代性能瓶颈往往是内存访问延迟,不是CPU计算能力。
  • 链表数据结构在现代高性能场景下通常不是好选择。
  • 应该选择缓存友好的数据结构,比如连续内存的数组、哈希表等。

std::list(双向链表)在性能上不理想:

std::list 的特点和性能瓶颈:

  • 双向链表结构:每个节点有指向前后节点的指针。
  • 每个节点独立分配内存:分散在堆的不同地址。
  • 遍历操作需要不断跳转指针:每访问一个节点都跳到内存中新的、不连续的位置。
  • 绝大多数遍历步骤都会产生缓存未命中(cache miss):CPU缓存机制极大依赖数据局部性,链表遍历完全打破了这一点。
  • 适用场景有限
    • 不常遍历链表
    • 频繁插入/删除操作,特别是在中间位置修改元素时。

总结:

  • 如果程序主要是遍历访问,std::list 性能非常差,因为缓存未命中导致CPU等待。
  • 如果程序是大量随机插入删除,但很少遍历,链表可能还合适。
  • 一般情况,推荐用连续内存结构(如 std::vectorstd::deque)替代 std::list,提升缓存效率和整体性能。

推荐用 std::vector 替代很多传统的数据结构,比如栈、队列、甚至映射(在某些场景下),原因主要有:

为什么“Just use std::vector”?

  • std::vector 是一个动态数组,内存连续,缓存友好,遍历快。
  • 栈(stack)std::vector 自带的 push_backpop_back 方法完美支持栈操作,性能极佳。
  • 队列(queue)
    • 如果队列大小有限或者生命周期短,用 std::vector + 一个“前端索引”管理队列头很有效。
    • 不断增长的 vector,用索引指向“队首”,避免频繁移动元素。
  • 映射(maps)
    • 在某些情况下,如果键空间有限或有序且访问模式简单,用 vector + 二分查找能替代。
    • 特别是小规模数据,vector 的线性遍历有时比树结构更快。

总结

  • std::vector 的缓存友好和简单高效的接口让它成为多数场景的首选。
  • 传统链表、复杂容器在缓存不友好或者频繁内存分配时开销大。
  • 利用索引技巧,vector 也可以实现队列功能。

为什么说使用 std::map 是“让代码变慢”的行为?

  • std::map 本质是一个平衡二叉树(通常是红黑树),节点通过指针连接。
  • 因此它具有链表结构的一些缺点:
    • 内存不连续,频繁指针跳转导致缓存不友好,遍历容易缓存失效(cache miss)。
    • 插入、删除操作涉及树的部分遍历和重平衡,这些操作复杂且成本较高。
  • 虽然 std::map 有提示(hint)功能帮助插入时减少遍历,但重平衡依然需要访问节点。
  • 这意味着相比简单的连续内存结构,std::map 速度较慢。

那么用 std::unordered_map 就好了?

  • std::unordered_map 是哈希表实现,查找速度通常比 std::map 快。
  • 但它也不是万能的,存在哈希冲突、内存分配和缓存效率等问题。
  • 具体使用时需要权衡应用场景和性能需求。
    总结
  • std::map 虽然提供了有序字典的功能,但牺牲了性能。
  • 在性能关键代码中,避免不必要使用 std::map,优先考虑更缓存友好的数据结构。
  • std::unordered_map 在大多数情况下性能更好,但也要注意哈希表的特性。

std::unordered_map 的实现细节

  • std::unordered_map 通常基于哈希桶(buckets)实现
  • 每个桶存储一串键值对,通常是一个链表(或者其他链式结构)。
  • 这意味着查找时常常涉及指针跳转(pointer chasing),导致缓存不友好,性能会受影响。

一个好的哈希表设计(理想实现)

  • 无桶设计(No buckets),也就是采用**开放寻址(open addressing)**技术。
  • 将键值对直接存储在一个连续内存区域(数组)中。
  • 遇到哈希冲突时,使用局部探测(local probing),尽量在同一缓存行内找到空位,减少缓存未命中的概率。
  • 保持键和值都尽可能小,进一步提高缓存利用率和访问速度。

总结

  • 传统 std::unordered_map 的链式桶结构,指针跳转带来性能损失。
  • 采用开放寻址的哈希表设计,能更好地利用CPU缓存,性能更优。
  • 这是很多高性能哈希表(例如 Google 的 dense_hash_map,Facebook 的 F14,或者其他自定义实现)采用的策略。

这部分内容传达了一个很重要的观念,打破了常见的误解:

传统观念 vs 现实

  • 传统观念说:
    • 效率(Efficiency) 主要靠 算法(Algorithms) 来提升。
    • 性能(Performance) 主要靠 数据结构(Data Structures) 来提升。
  • 现实是:
    • 这个划分其实是假的,两者紧密耦合,彼此影响。

数据结构与算法的关系

  • 它们密不可分,必须同时考虑,才能取得最佳效果。
  • 正如 Wirth(计算机科学先驱者)所著的书中指出,算法和数据结构是不可分割的一体。
  • 算法不仅仅影响时间复杂度,也会影响数据访问的模式,比如访问缓存的局部性。
  • 有些算法看起来效率低(比如冒泡排序),但在某些场景下却因为数据访问模式好反而表现不错。
  • 类似地,哈希算法(比如 cuckoo hashing)也是算法和数据结构结合的典型例子。

总结

你需要同时关注:

  • 算法本身的工作量(效率)
  • 数据访问的方式和结构设计(性能)
    两者结合,才能打造真正高效且快速的程序。

结论要点

  • 效率(Efficiency)和性能(Performance)同样重要,尤其是现在。
  • C++ 给予你掌控效率和性能的能力。
  • 一定要重视你的算法设计!
  • 养成良好习惯,使用 API 时避免无谓的浪费。
  • 优先使用连续、密集、面向缓存的高效数据结构(比如 std::vector)。
  • 享受用 C++ 写出疯狂快的代码的过程!
    这种思路帮助你不仅写出正确的代码,更写出跑得飞快的代码。
http://www.xdnf.cn/news/10560.html

相关文章:

  • Linux中的System V通信标准-共享内存、消息队列以及信号量
  • 工作流引擎-18-开源审批流项目之 plumdo-work 工作流,表单,报表结合的多模块系统
  • isp中的 ISO代表什么意思
  • 【机器学习基础】机器学习入门核心算法:多分类与多标签分类算法
  • Vue3(watch,watchEffect,标签中ref的使用,TS,props,生命周期)
  • vue · 路由传参query和params
  • 50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | Sound Board(音响控制面板)
  • 【Linux网络】传输层TCP协议
  • Sxer.Base.Debug(打印调试)
  • 腾答知识竞赛系统功能介绍
  • 【Java】泛型
  • 线性代数复习
  • Bootstrap 5学习教程,从入门到精通,Bootstrap 5 安装及使用(2)
  • CNN卷积网络:让计算机拥有“火眼金睛“(superior哥AI系列第4期)
  • Linux——计算机网络基础
  • 分布式锁剖析
  • 微软markitdown PDF/WORD/HTML文档转Markdown格式软件整合包下载
  • React Hooks 与异步数据管理
  • YARN应用日志查看
  • demo_win10配置WSL、DockerDesktop环境,本地部署Dify,ngrok公网测试
  • CppCon 2014 学习:0xBADC0DE
  • FFmpeg移植教程(linux平台)
  • NTP库详解
  • AI矢量软件|Illustrator 2025网盘下载与安装教程指南
  • Java从入门到精通 - 常用API(一)
  • 【Hot 100】70. 爬楼梯
  • 【农资进销存专用软件】佳易王农资进出库管理系统:赋能农业企业高效数字化管理
  • 94. Java 数字和字符串 - 按索引获取字符和子字符串
  • java28
  • 随记 nacos + openfegin 的远程调用找不到服务