读程序员的制胜技笔记09_死磕优化(下)

这篇具有很好参考价值的文章主要介绍了读程序员的制胜技笔记09_死磕优化(下)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

读程序员的制胜技笔记09_死磕优化(下)

1. 造成延迟的3个方面

1.1. CPU

1.2. I/O

1.3. 人

2. 不要打包数据

2.1. 一个打包的数据结构

2.1.1. C#

struct UserPreferences {

  public byte ItemsPerPage;
  public byte NumberOfItemsOnTheHomepage;
  public byte NumberOfAdClicksICanStomach;
  public byte MaxNumberOfTrollsInADay;
  public byte NumberOfCookiesIAmWillingToAccept;
  public byte NumberOfSpamEmailILoveToGetPerDay;
}

2.1.2. 由于CPU对未对齐边界的内存地址的访问速度较慢,你节省空间的好处就被这个速度损失给抵消了

2.1.3. 把结构中的数据类型从byte改为int,并创建一个基准测试来测试二者差异,你可以看到byte的访问时间几乎是int的两倍,尽管它只占用了1/4的内存

2.1.4. 要避免不必要地优化内存

2.2. 对齐是指内存地址为4、8、16等2的倍数,至少是CPU的字大小(word size)

2.3. CPU的字大小

2.3.1. 由CPU一次能处理多少比特的数据来决定

2.3.2. 与CPU是32位还是64位的密切相关

2.3.3. 字大小主要反映了CPU的累加寄存器的大小

2.4. CPU从没有对齐的内存地址(unaligned memory addresses)读取数据时,会造成“惩罚”

2.4.1. 从某一个内存地址,比如1023,读取数据的时间会比从内存地址1024读取数据的时间要长

2.5. 有些CPU根本不允许你访问未对齐的内存地址

2.5.1. Amiga计算机中用到的Motorola 68000和一些基于ARM的处理器

2.6. 编译器通常会处理好对齐问题

3. 就地取材

3.1. 缓存是指将经常使用的数据保存在同一个位置,这个位置相较其他位置来说,访问速度更快

3.2. CPU有自己的缓存存储器,访问速度各不相同,但都比RAM的访问速度快

3.2.1. 顺序读取比随机读取内存的速度要快

3.2.2. 顺序读取一个数组可以比顺序读取一个链表更快,尽管两者从头到尾读取一遍需要的时间都为O(N),但数组的性能比链表更好,原因是数组的下一个元素在内存的缓存区的可能性更大

3.3. CPU通常猜你是按顺序读取数据的

3.4. 链表中的元素在内存中是分散的,因为它们是单独分配的

3.4.1. 并不意味着链表没有用,它有很好的插入/删除性能,而且当它增长时,内存开销较少

3.5. 在大多数情况下,列表对你来说是最优选择,而且它的读取速度更快

4. 将依赖性工作分开

4.1. CPU指令是由处理器上不连续的单元处理的

4.2. 管线(pipelining)

4.2.1. 由于解码单元需要等待指令完成,它可以在内存访问时为下一条指令做解码工作

4.2.2. CPU可以在单个内核上并行执行多条指令,只要下一条指令不依赖于前一条指令的结果

4.3. 重新排序指令,减少代码之间的依赖性,这样一条指令就不会因为依赖上一个操作的结果而阻塞管线上的下一条指令

4.3.1. 可以帮你提高代码的运行速度,因为强依赖代码会阻塞管线

5. 要有可预测性

5.1. 分支预测(branch prediction)

5.2. 到了编译阶段,它们都会变成一堆比较、加法和分支操作

5.3. 机器语言,即CPU能看懂的原生语言,由一连串的数字组成

5.4. 汇编语言是由机器语言转换过来,让人能够看懂的语言

5.4.1. 当你需要了解CPU计算密集型的任务时,汇编语言尤其能发挥神奇的作用

5.4.2. 只要你熟悉汇编语言的结构,就可以阅读JIT编译器生成的机器代码,并了解其实际行为

5.5. 汇编语法在不同的CPU架构中有所不同,建议至少要熟悉一种

5.5.1. 它将减少你对“黑箱”内发生的事情的恐惧

5.5.2. 它可能看起来很复杂

5.5.3. 但它比我们写程序的语言更简单,甚至可以说是原始的

5.6. 在执行之前,CPU不可能知道compare指令是否会执行成功,但由于有了分支预测,它可以根据观察到的情况做出较为靠谱的预测

5.7. 根据它的预测,CPU开始处理它预测的那个分支的指令,如果它预测成功,那之前做的准备就派上了用处,提高了性能

5.8. 你给CPU的“惊喜”越少,它的表现就越好

5.8.1. 由随机数组成的数组会比较慢的原因:在这种情况下,分支预测会派不上用场

6. SIMD

6.1. 单指令、多数据(single instruction,multiple data,SIMD)

6.2. CPU也支持专门的指令,可以用一条指令同时对多个数据进行计算

6.3. 对多个变量进行相同的计算,SIMD可以在其支持的架构上大大提升其性能

6.4. 当你有一个计算密集型的任务,需要同时对多个元素进行相同的操作时,你可以考虑使用SIMD

6.5. C#通过System.Numerics命名空间中的Vector类型提供SIMD功能

6.6. 有些CPU根本不支持SIMD,所以你必须先检查CPU上是否有这个功能

6.6.1. C#

if (!Vector.IsHardwareAccelerated) {

//这里是非向量实现
}

6.7. CPU在同一时间可以处理多少个给定的类型。这在不同的处理器上是不同的,所以你必须先查询一下

6.7.1. int chunkSize = Vector<int>.Count;

6.7.2. 当你知道你一次可以处理的数量时,你可以直接分块处理缓冲区(buffer in chunk)

6.8. 经典的简单乘法

6.8.1. C#

public static void MultiplyEachClassic(int[] buffer, int value) {

  for (int n = 0; n < buffer.Length; n++) {
    buffer[n] *= value;
  }
}

6.9. Vector类型乘法

6.9.1. C#

public static void MultiplyEachSIMD(int[] buffer, int value) {

  if (!Vector.IsHardwareAccelerated) {
    MultiplyEachClassic(buffer, value);  ⇽--- 如果CPU不支持SIMD,则调用之前的普通方法来实现
  }
 
  int chunkSize = Vector<int>.Count;  ⇽--- 查询SIMD一次可以处理多少值
  int n = 0;
  for (; n < buffer.Length - chunkSize; n += chunkSize) {
    var vector = new Vector<int>(buffer, n);  ⇽--- 将数组段复制到SIMD寄存器当中
    vector *= value;  ⇽--- 一次性乘所有的值
    vector.CopyTo(buffer, n);  ⇽--- 替换结果
  }
 
  for (; n < buffer.Length; n++) {  ⇽--- 用普通的方法处理剩余的字节
    buffer[n] *= value;
  }  ⇽--- 
}

6.9.2. 基于SIMD的代码的运行速度是普通代码的约两倍

6.9.3. 根据你处理的数据类型和你对数据进行的操作,它还可以更快

7. I/O

7.1. I/O包含CPU与外围硬件沟通的一切,比如磁盘、网络适配器,还有GPU

7.2. I/O通常是性能链上最慢的环节

7.2.1. 缓慢源自物理学,但硬件可以独立于CPU运行,所以它可以在CPU做其他工作时保持运行

7.2.2. 可以把CPU和I/O的工作重叠起来,在更小的时间范围内完成整体操作

7.3. 键盘是字符设备,因为它一次发送一个字符

7.4. 许多I/O设备都是以块的形式进行读写的,称为块设备(block device)

7.4.1. 网络和存储设备通常是块设备

7.4.2. 块设备不能读取小于块大小的东西,所以读取小于典型块大小(typical block size)的东西不合理

7.5. 缓冲区大小对I/O性能的影响

7.5.1. 图

读程序员的制胜技笔记09_死磕优化(下)文章来源地址https://www.toymoban.com/news/detail-746157.html

7.5.2. 使用512字节的缓冲区也会产生巨大的作用,复制操作的速度提高了6倍

7.5.3. 将其增加到256KB时效果最好,再大一点也只能产生边际改善

7.5.3.1. 在256KB之后,提升突然变得杯水车薪

7.5.4. Windows I/O使用256KB作为其I/O操作和缓冲区管理的默认缓冲区大小

7.6. 当你处理I/O时,找到理想的缓冲区大小,并避免分配超过你需要的内存

8. 异步I/O

8.1. 它经常与多线程相混淆,多线程是一种并行化模型,通过让一个任务在不同的核心上运行,使得操作的速度变快

8.2. 多线程和异步I/O也可以一起使用,因为它们分别解决不同场景的痛点

8.3. I/O自然是异步的,因为外部硬件几乎总是比CPU响应慢,而且CPU不喜欢等待

8.4. 中断和直接存储器访问(direct memory access,DMA)等机制的发明是为了让硬件在I/O操作完成后向CPU发出信号,以便CPU能够传输结果

8.5. 异步I/O是一种仅适用于I/O重度操作的并行化模型,它可以在单个核心上工作

8.6. 异步I/O可以并行地运行多个I/O操作并收集结果,而不需要忍受多线程带来的问题

8.7. 异步代码还可以帮助提高事件驱动机制(event-driven mechanism),特别是用户界面的响应速度,完成这个操作也不需要消耗线程

8.8. 直到2010年初,异步I/O还是使用回调函数管理的

8.9. async/await语义来编写异步I/O代码的好方法

8.10. 你开始的异步操作越多,你就越容易失去对操作的控制

8.10.1. "回调地狱”(callback hell)

8.11. 如果你的异步代码在等待某些东西完成,你就做错了

9. 缓存

9.1. 缓存是立即提高性能的最有力的方法之一

9.2. 缓存失效可能是个难题,但如果你只缓存你不担心失效的东西,那它就不是个问题

9.3. 不需要使用在如Redis或Memcached这样的单独服务器上的复杂缓存层,你可以使用内存缓存(in-memory cache)

9.3.1. 微软在System.Runtime.Caching包中提供的MemoryCache类

9.4. 避免使用那些不是为缓存设计的数据结构

9.4.1. 它们通常没有任何驱逐(eviction)或过期机制,从而成为内存泄露的来源,并最终导致程序崩溃

9.5. 不要害怕缓存会无限存在

9.5.1. 无论是缓存的驱逐还是应用程序的重启都会在世界末日前发生

10. 要点

10.1. 把不成熟的优化作为练习,并从中学习

10.2. 避免为了优化而优化,这会把自己带入“深坑”

10.3. 始终用基准测试来验证你的优化

10.4. 保持优化和响应性的平衡

10.5. 在构建数据结构时,尽可能做到内存对齐,以获得更好的性能

10.6. 配备缓存定位、管线和SIMD

10.7. 使用适当的缓冲机制来提高I/O性能

10.8. 使用异步编程来并行运行代码和I/O操作,不浪费线程

10.9. 在紧急情况下,放弃缓存方案

到了这里,关于读程序员的制胜技笔记09_死磕优化(下)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 读程序员的制胜技笔记06_测试(下)

    1.3.1.1. 假设18岁是你游戏用户的法定年龄 1.3.2.1. C# 1.3.2.1.1. 不需要测试公元1年1月1日到9999年12月31日之间所有可能的DateTime值(有360多万个) 1.3.2.1.2. 只需要测试7个不同的输入 1.3.2.1.3. 通过条件语句将输入范围进行分割的操作称为“边界条件”(boundary conditional) 1.3.2.1.3.1. 定

    2024年02月05日
    浏览(48)
  • 读程序员的制胜技笔记14_安全审查(下)

    1.2.2.1. 看不出来是什么?那我拒绝为你服务 1.4.1.1. 工作量证明相当消耗客户端的运算资源,对那些性能较低的设备不友好,并且它还会影响设备电池的使用寿命 1.4.1.2. 有可能会严重降低用户体验,其后果甚至比验证码的还要恶劣 3.5.2.1. 存储需求更少,性能更强,数据管理

    2024年02月05日
    浏览(45)
  • 读程序员的制胜技笔记10_可口的扩展

    2.8.3.1. 纯函数有一个好处,它们是100%线程安全的 2.9.1.1. 这套数据结构并不都是无锁的 2.9.1.2. 虽然它们依然使用锁,但它们是被优化过的,锁的持续时间会很短,保证了其速度,而且它们可能比真正的无锁替代方案更简单 2.9.2.1. 其中原始数据从未改变,但每个修改操作都

    2024年02月05日
    浏览(67)
  • 读程序员的制胜技笔记13_安全审查(上)

    5.6.1.1. 任何你不想丢失或泄露的东西都是资产,包括你的源代码、设计文档、数据库、私钥、API令牌、服务器配置,还有Netflix观看清单 5.6.2.1. 每台服务器都会被一些人访问,而每台服务器都会访问其他一些服务器 6.1.1.1. 设计时首先要考虑到安全问题,因为在既有基础上去

    2024年02月05日
    浏览(62)
  • 读程序员的制胜技笔记02_算法与数据结构

    3.1.1.1. 根据你的需要,可以有更智能的算法 3.1.3.1. 算法本身并不意味着它很聪明 3.2.1.1. public static bool Contains(int[] array, int lookFor) { for (int n = 0; n < array.Length; n++) {        if (array[n] == lookFor) {            return true;        }    }    return false; } 3.3.1.1. public sta

    2024年02月06日
    浏览(62)
  • 读程序员的制胜技笔记12_与Bug共存(下)

    2.2.1.1. 故障代码(failing code)放在一个try语句块里,然后加上一个空的catch语句块,就大功告成了 2.2.1.2. 开发者为整个应用程序添加了一个通用的异常处理程序,但实际上这个程序的工作原理就是忽略所有的异常,也就防止所有的崩溃 2.2.1.3. 如果像那样添加一个空的处理程序

    2024年02月05日
    浏览(58)
  • 读程序员的制胜技笔记11_与Bug共存(上)

    2.7.3.1. 在构造时验证其有效性,这样一来就不可能包含无效值 2.8.2.1. 其主张一个花括号与声明在同一行 2.9.1.1. 看看这些现成的类型 2.9.3.1. 它代表持续时间 2.9.3.2. 你没有理由用TimeSpan以外的任何东西来表示持续时间,即使你所调用的函数不接受TimeSpan作为参数 2.9.4.1. 它也

    2024年02月05日
    浏览(56)
  • 读程序员的制胜技笔记04_有用的反模式(下)

    1.3.1.1. 自己做自己的甲方 3.2.2.1. 紧耦合(tight coupling) 3.2.2.2. 依赖性是万恶之源 3.3.7.1. 因为你可能需要用接口而不是具体的引用来定义依赖关系,但这也会使代码摆脱依赖关系 5.2.3.1. 没有其他错误发生时执行的代码部分 5.3.3.1. 退出点(exit point)是指函数中导致其返回给调用

    2024年02月06日
    浏览(86)
  • 读程序员的制胜技笔记03_有用的反模式(上)

    4.5.4.1. 你在物理数据库结构上增加了一个依赖项 4.5.4.2. 如果你需要改变信息表的布局或所使用的数据库技术,你就必须检查所有的代码,确保所有的东西都能与新的表布局或新的数据库技术一起工作 4.5.6.1. 这是保持组件或整个层尽可能简单的关键 4.8.3.1. 每个成员只对自己

    2024年02月06日
    浏览(47)
  • 读程序员的README笔记09_代码评审

    4.4.1.1. walk-through 4.4.1.2. 一种面对面的会议,开发人员在会上共享他们的屏幕,并引导队友了解正在进行的修改内容 4.4.1.3. 是启发想法和让你的团队适应代码修改的好方法 5.1.2.1. 如果紧急度不明确,请询问提交者 5.4.4.1. SQL注入攻击、敏感数据泄露和跨站脚本攻击的漏洞

    2024年02月05日
    浏览(54)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包