技术

《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go细节 codereview mat使用 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 MVCC 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes自动扩容缩容 神经网络模型优化 直觉上理解机器学习 knative入门 如何学习机器学习 神经网络系列笔记 TIDB源码分析 《阿里巴巴云原生实践15讲》笔记 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 副本一致性 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全机制 jvm crash分析 Kubernetes监控 Kubernetes 控制器模型 Prometheus 学习 容器日志采集 容器狂占cpu怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes objects之编排对象 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 kubernetes实践 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 《深入剖析kubernetes》笔记 精简代码的利器——lombok 学习 编程语言的动态性 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 分布式计算系统的那些套路 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 mysql 批量操作优化 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS2——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 硬件对软件设计的影响 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记2 《mysql技术内幕》笔记1 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 Scala的一些梗 OpenTSDB 入门 spring事务小结 事务一致性 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型 java exception Linux IO学习 network channel network byte buffer 测试环境docker化实践 netty(七)netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Macvlan Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker中涉及到的一些linux知识 hystrix学习 Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty(六)netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 Redis概述 机器学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 异构数据库表在线同步 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM内存与执行 git spring rmi和thrift maven/ant/gradle使用 再看tcp 缓存系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 VPN(Virtual Private Network) Kubernetes存储 Kubernetes 其它特性 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 使用etcd + confd + nginx做动态负载均衡 如何通过fleet unit files 来构建灵活的服务 CoreOS 安装 CoreOS 使用 Go学习 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

openkruise 学习 kubernetes crd 及kubebuilder学习 pv与pvc实现 csi学习 client-go学习 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 《推荐系统36式》笔记 资源调度泛谈 系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 《聊聊架构》 书评的笔记 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 用户登陆 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列

标签


硬件对软件设计的影响

2018年01月07日

简介

物质基础决定上层建筑,在计算机世界里,硬件作为“物质基础”深刻的影响着软件设计和性能。

国外一个大牛的博客 Mechanical Sympathy Hardware and software working together in harmony 讲的是底层硬件是如何运作的,以及与其协作而非相悖的编程方式。剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充 作为一个开发者你可以逃避不去了解CPU、数据结构或者大O符号 —— 而我用了10年的职业生涯来忘记这些东西。但是现在看来,如果你知道这些知识并应用它,你能写出一些非常巧妙和非常快速的代码。

2019.4.12 补充:进程管理信息数据结构 二进制文件分段 ==> 进程分段 ==> 指令操作码/操作数 ==> cpu运算单元/数据单元 ==> cpu代码段寄存器/数据段寄存器/堆栈段寄存器等,从这个视角看,又有一种软硬件融合的味道。

并行计算能力

Java和操作系统交互细节

CPU 的指令执行

一行代码能够执行,必须要有可以执行的上下文环境,包括:指令寄存器、数据寄存器、栈空间等内存资源。

为什么可以流水线/乱序执行?我们通常会把 CPU 看做一个整体,把 CPU 执行指令的过程想象成,依此检票进站的过程,改变不同乘客的次序,并不会加快检票的速度。所以,我们会自然而然地认为改变顺序并不会改变总时间。但当我们进入 CPU 内部,会看到 CPU 是由多个功能部件构成的。一条指令执行时要依次用到多个功能部件,分成多个阶段,虽然每条指令是顺序执行的,但每个部件的工作完成以后,就可以服务于下一条指令,从而达到并行执行的效果。比如典型的 RISC 指令在执行过程会分成前后共 5 个阶段。

  1. IF:获取指令;
  2. ID(或 RF):指令解码和获取寄存器的值;
  3. EX:执行指令;
  4. ME(或 MEM):内存访问(如果指令不涉及内存访问,这个阶段可以省略);
  5. WB:写回寄存器。

在执行指令的阶段,不同的指令也会由不同的单元负责。所以在同一时刻,不同的功能单元其实可以服务于不同的指令。

从JVM并发看CPU内存指令重排序(Memory Reordering)我们知道现代CPU的主频越来越高,与cache的交互次数也越来越多。当CPU的计算速度远远超过访问cache时,会产生cache wait,过多的cache wait就会造成性能瓶颈。针对这种情况,多数架构(包括X86)采用了一种将cache分片的解决方案,即将一块cache划分成互不关联地多个 slots (逻辑存储单元,又名 Memory Bank 或 Cache Bank),CPU可以自行选择在多个 idle bank 中进行存取。这种 SMP(指在一个计算机上汇集了一组处理器,各CPU之间共享内存子系统以及总线结构) 的设计,显著提高了CPU的并行处理能力,也回避了cache访问瓶颈。

Memory Bank的划分:一般 Memory bank 是按cache address来划分的。比如 偶数adress 0×12345000分到 bank 0, 奇数address 0×12345100分到 bank1。

重排序的种类

  • 编译期重排。编译源代码时,编译器依据对上下文的分析,对指令进行重排序,使其更适合于CPU的并行执行。
  • 运行期重排,CPU在执行过程中,动态分析依赖部件的效能(CPU0检查 bank0 的可用性,发现 bank0 处于 busy 状态,那么本来写入cache bank0的数据操作会延后),对指令做重排序优化。

前者是编译器进行的,不同语言不同。后者是cpu 层面的,所有使用共享内存模型进行线程通信的语言都要面对的。

多层次内存结构

RAM 是随机访问内存,就是给一个地址就能访问到数据,而磁盘这种存储媒介必须顺序访问,而 RAM 又分为动态和静态两种,静态 RAM 由于集成度较低,一般容量小,速度快,而动态 RAM 集成度较高,主要通过给电容充电和放电实现,速度没有静态 RAM 快,所以一般将动态 RAM 做为主存,而静态 RAM 作为 CPU 和主存之间的高速缓存 (cache),用来屏蔽 CPU 和主存速度上的差异,也就是我们经常看到的 L1 , L2 缓存。每一级别缓存速度变低,容量变大。

缓存速度的差异

从CPU到 大约需要的 CPU 周期 大约需要的时间
主存   约60-80纳秒
QPI 总线传输(between sockets, not drawn)   约20ns
L3 cache 约40-45 cycles, 约15ns
L2 cache 约10 cycles, 约3ns
L1 cache 约3-4 cycles, 约1ns
寄存器 1 cycle  

当CPU执行运算的时候,它先去L1查找所需的数据,再去L2,然后是L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。如果你的目标是让端到端的延迟只有 10毫秒,而其中花80纳秒去主存拿一些未命中数据的过程将占很重的一块。如果你在做一些很频繁的事,你要确保数据在L1缓存中

当然,缓存命中率是很笼统的,具体优化时还得一分为二。比如,你在查看 CPU 缓存时会发现有 2 个一级缓存,这是因为,CPU 会区别对待指令与数据。虽然在冯诺依曼计算机体系结构中,代码指令与数据是放在一起的,但执行时却是分开进入指令缓存与数据缓存的,因此我们要分开来看二者的缓存命中率。

  1. 提高数据缓存命中率,考虑cache line size
  2. 提高指令缓存命中率,CPU含有分支预测器,如果分支预测器可以预测接下来要在哪段代码执行(比如 if 还是 else 中的指令),就可以提前把这些指令放在缓存中,CPU 执行时就会很快。例如,如果代码中包含if else,不要让每次执行if else 太过于随机。

提升多核 CPU 下的缓存命中率:一、二级缓存是每颗核心独享的,如果一个进程每次都被调度到不同的cpu 核心上执行, 则上一个时间片 在某个核心 上load 的缓存 就完全复用不上了。因此,操作系统提供了将进程或者线程绑定到某一颗 CPU 上运行的能力。

缓存的存取——缓存行

高性能队列——Disruptor

cpu和内存的速度差异 ==> 缓存 ==> 多级缓存 ==> Cache是由很多个cache line组成的。每个cache line通常是64字节,并且它有效地引用主内存中的一块儿地址。CPU每次从主存中拉取数据时,会把相邻的数据也存入同一个cache line。也就是说,假设一个cache line 对应内存地址是0x1000,存着一个volatile变量,你改了这个变量,那么跟它挨着的另一个变量(地址为0x1008)也会失效(假设它们同属于一个java对象内存结构,或都是某个数组的元素)因为整个cache line被标记为失效了。下次访问第二个变量时,便需要从内存中加载到缓存,再加载到cpu。从某种程度上可以说:cpu一直是批量访问缓存/内存的

因此,缓存行中的64byte 数据,一个失效全失效,有时会带来一些性能问题。

JVM4——《深入拆解java 虚拟机》笔记 2018年07月20日因为 缓存行,jvm 使用了字段内存对齐机制。

volatile 字段和缓存行也有一番故事

多级缓存带来的问题

  1. 所谓线程 安全,最后可以归结到 并发读写 问题。参见 多线程
  2. 所谓数据结构,最后可以归结到 读写效率的 权衡问题。 参见hbase 泛谈 数据信息 和 结构信息(提高读写效率)混在一起,因为磁盘的缘故, 顺序写即可提高读效率。而查询/读效率 的提高花活儿就比较多了,并且通常 会降低写效率。

基于共享内存的数据通信问题从某种视角来看,共享内存模型下的并发问题都是“共享内存,但不共享寄存器/L1/L2缓存”导致的。

多级缓存问题的部分解决——内存屏障

Compiler 和 cpu 经常搞一些 optimizations,这种单线程视角下的优化在多线程环境下是不合时宜的,为此要用 memory barriers 来禁止 Compiler 和 cpu 搞这些小动作。 For purposes here, I assume that the compiler and the hardware don’t introduce funky optimizations (such as eliminating some “redundant” variable reads, a valid optimization under a single-thread assumption).

老司机谈技术天花板——做自己的破壁人

硬件为了加快速度,会弄各种缓存,然后就有一个缓存/副本一致性问题,也会定一些一致性规则(什么时候同步之类)。但基本就是,你不明确要求,硬件就当缓存是有效的。那么就有了打掉缓存的指令(即强制同步),然后编译器/runtime 支持该指令,最终反映在语言层面上。

Java和操作系统交互细节插入内存屏障的指令,会根据指令类型不同有不同的效果,例如在 monitorexit 释放锁后会强制刷新缓存,而 volatile 对应的内存屏障会在每次写入后强制刷新到主存,并且由于 volatile 字段的特性,编译器无法将其分配到寄存器,所以每次都是从主存读取,所以 volatile 适用于读多写少得场景

为什么会有人觉得优化没有必要,因为他们不理解有多耗时

Teach Yourself Programming in Ten Years

Remember that there is a “computer” in “computer science”. Know how long it takes your computer to execute an instruction, fetch a word from memory (with and without a cache miss), read consecutive words from disk, and seek to a new location on disk.

Approximate timing for various operations on a typical PC:

  耗时
execute typical instruction 1/1,000,000,000 sec = 1 nanosec
fetch from L1 cache memory 0.5 nanosec
branch misprediction 5 nanosec
fetch from L2 cache memory 7 nanosec
Mutex lock/unlock 25 nanosec
fetch from main memory 100 nanosec
send 2K bytes over 1Gbps network 20,000 nanosec
read 1MB sequentially from memory 250,000 nanosec
fetch from new disk location (seek) 8,000,000 nanosec
read 1MB sequentially from disk 20,000,000 nanosec
send packet US to Europe and back 150 milliseconds = 150,000,000 nanosec
上下文切换 数千个CPU时钟周期,1微秒

单核CPU技术瓶颈 ==> CPU 向多核发展 ==> 多台服务器

2019.3.28 补充

  1. 语言层面,golang协程、java9 支持反应式等
  2. 架构层面,全异步化、反应式架构、分布式计算

通过语言层、框架层提出新的模型,引导写出并行度高的代码