编码技巧 笔记

1012编写可读代码的艺术 阅读笔记

表面改进

1.代码应当易于理解

可读性: 代码的写法应当使别人理解它所需的时间最小化

2.将信息装入名字里

  • 选择专业的词
  • 避免泛泛的名字
  • 用具体的名字代替抽象的名字
  • 使用前缀/后缀给名字附带更多信息
  • 决定名字的长度
    • 小作用域短名字
    • 首字母缩略词– 需要易于理解
    • 丢掉没用的词
  • 利用名字的格式表达含义

3.不会误解的名字

4.审美

  1. 使用一致的布局,让读者很快习惯这种风格
  2. 让相似的代码看上去相似
  3. 把相关的代码行分组,形成代码块

5.该写什么样的注释

不该写什么注释?

  1. 不要为那些从代码本身就能快速推断的事实写注释
  2. 不要为了注释而注释
  3. 不要给不好的名字加注释–应该把名字改好

该写什么?

  1. 记录思想

    记录写代码时有过的重要想法

    1. 比如加入”导演评论”
  2. 为代码中的瑕疵写注释

  3. 给常量加注释

  4. 公布可能的陷阱

  5. 全局观注释–类之间如何交互等

6.写出言简意赅的注释

  1. 让注释保持紧凑
  2. 避免不明确的代词
  3. 润色粗糙的句子
  4. 精确描述函数行为
  5. 用输入输出例子来说明
  6. 声明代码意图
  7. 采用信息量高的词

逻辑改进

7.把控制流变的易读

  1. 比较语句

    左侧倾向于变化值,右侧倾向于固定值

  2. 最小化嵌套

    提前返回

    这是个好事

8.拆分超长表达式

思想:拆成小块

9.变量与可读性

  • 减少变量
    • 去掉没有价值的临时变量
    • 减少中间结果
    • 减少控制流变量
    • 缩小变量的作用域

重新组织代码

10.抽取不相关的子问题

如果一段代码并不是为了这个代码块的高层次目标直接工作,可以将其抽取处理.

11.一次只做一件事

如题

12.把想法变成代码

最好按自然理解的逻辑组织代码

csapp 异常控制流

1013 异常控制流

Exception Control Flow ECF

1.1 异常处理

启动时生成异常表

运行时检测到异常,确定异常号,查找异常表,进入处理程序

执行完后,执行一条”从中断返回”指令,将控制返回给被中断的程序[如果是一个用户程序被中断]

1.2异常类别

  1. 中断

  2. 陷阱和系统调用

    陷阱: 有意的异常,用来在用户程序和内核之间提供一个像过程一样的接口–系统调用

  3. 故障

    如 除法错误,却也,一般保护故障[程序引用了一个未定义的虚拟内存区域,Linux一般会报告为一个 段故障 Segmentation fault]

  4. 终止

    不可恢复的致命错误–通常硬件错误

2 进程

定义

一个执行中程序的实例

维护一个 该程序是系统中当前运行的唯一程序的假象

拥有

  • 一个独立的逻辑控制流
    • 提供独占处理器的假象
  • 一个私有的地址空间
    • 提供独占内存系统的假象

进程空间

给出通用结构

以x86-64 Linux为例

image-20221013142209341

上下文切换

用于实现多任务

上下文: 重新启动一个被抢占的进程所需的状态

系统调用错误处理

Unix系统级函数遇到错误时,通常会返回-1,并设置全局整数变量errno来表示什么出错了.

进程控制

重点是创建子进程

父进程调用fork函数创建一个子进程

父子进程区别

相同

  • 虚拟地址空间
    • 代码
    • 数据段
    • 共享库
    • 用户栈
  • 打开描述符
    • 意味着父进程调用fork时,子进程可以读写父进程中打开的任何文件

区别

  • Pid
    • 进程id不同
  • 并发执行
    • 父进程和子进程是并发运行的独立进程
  • 虚拟地址空间
    • 子进程得到的副本和父进程相同,包括打开的文件描述符,但是是一份独立的副本,也就是说,之后的调用,子进程和父进程独立.

fork

  • 调用一次返回两次
    • 一次是在调用进程(父进程中)
      • 返回子进程PID(非零)
    • 一次在创建的进程中(子进程中)
      • 返回0
  • 可以用来辨认子进程还是父进程

回收子进程

步骤: 终止->分配给init进程作为养父->父进程回收[若父进程没来得及回收,则由init进程回收]

信号

较高层

  • 是一个小消息,通知进程系统中发生了一个某种类型的事件.
  • 用于通知用户进程发生了一些低层的硬件异常

编码技巧 笔记

1 整洁性

  • 语义简单明确,优先考虑易于读者理解的写法
  • 简洁!=代码短
    • 复杂的问号表达式反而不如 if else 方便理解
      • 个人疑问:问号表达式可以一定程度上提供条件数据传送,而非条件转移
  • 提前返错
    • 提前返回错误判断可以减少主体逻辑的缩进数量,使主体代码逻辑更醒目
  • 利用析构函数做清理工作
    • 利用C++析构函数做清理工作,在复杂冗长代码中不会漏掉. 比如执行回调,关闭文件,释放内存
  • 用朴素直观的算法
    • 在非关键路径上,优先使用朴素直观,维护性好的代码.
  • 用轮询代替条件变量
    • 非关键路径上这么做,代码间接,不容易出bug
    • 轮询: 一直等待信号
  • 在关键对象增加magic字段
    • 增加magic字段和断言检查,可以及时发现内存错误

2 测试

  • 边界

  • 状态/分支测试

  • 重复/幂等性测试

  • 兼容性测试

  • 防御性测试

    • 关注系统在最差情况下的表现,明确能力边界
  • 避免写出不稳定case

      • 测试不聚焦,无脑复制粘贴,等价类测试爆炸
      • 异步等待,基于时间假设,sleep 并发,未能在预期的窗口期交互
      • 有顺序依赖的测试,共享某个状态
      • 资源溢出,数据库链接满、内存 OOM 析构随机 core
      • 析构未严格保序或者未构造
      • 多线程共享资源的错误用法导致概率 crash
      • 有未处理完的任务就退出

3 提交

  1. 一次提交不要超过400行代码,最好只解决一个问题
  2. 自我检查
    1. 速度<500行/小时
    2. 一次review时间不超过1小时
    3. 接口>测试>实现

4 高效工作方法

  • 抽象和分而治之

    • 抽象:明确模块之间的依赖关系,确定API接口
    • 分而治之:对子系统设计进行合理的注释,帮助理解
    • 代码提交尽量做到原子[不可分割的特性.修复.优化],测试代码一同提交
  • 不要重复

    • 寻找重复逻辑和代码,并进行封装
    • 寻找流程重复,使用脚本或者工具自动化
    • 沉淀踩坑经验
  • 快速迭代

    • 不要过度设计
    • 尽快让代码运行和快速验证,不断迭代来完善
    • 为了快速验证,本地测试成本低
    • 实现一个可运行的脚手架,再持续添加内容
  • 忌“太心急”,慢即是快

    • 需求澄清:类似 TCP 三次握手,用自己理解的方式再给对方讲一遍,确认双方理解一致,对焦,避免重复返工
    • 自我提问:为什么做这件事?业务价值是什么?关键技术是什么?已有的系统和它对比有什么不同?兄弟团队是否做过类似的工作?是否有经验可供参考?业务/技术的适用场景是什么?预计耗时和进度风险?
    • 新人往往脚踏实地,忘记了仰望星空,只顾着埋头苦干,不思考背后的业务价值,这一锄头,那一铁锹,遍地都是坑,就是不开花,费时费力,成就感低。
  • 忌低效沟通,用数据说话

    • 精确地描述问题,上下文和范围,提供有效信息
    • 文档是提高沟通效率的最佳方式之一,Google 有文档文化,推荐阅读《Design Docs at Google》[5]
    • Bad Case:「测试 CX6 网卡时,IOPS 大幅下降」
    • Good Case:「在 100g 网络标卡 CX6 验证性能时,8 jobs 32 depth iosize 4K 场景下,极限 IOPS 从 120 万下降至 110 万,与 FIC 卡相比性能存在 8% 差异」
  • 忌“蠢问题”,学会提问

    • 鼓励新人多提问,但提问的问题一定要有质量
    • 关于如何提出一个好问题推荐阅读《提问的智慧》[6]
    • Bad Case:「我在编译耗时很长,我怀疑是资源不够,这种情况怎么办?」
    • Good Case:「我的开发机编译耗时 2 小时,不符合预期,OS 是 centOS 7U、128GB 内存、64Core,编译并发度是 20 核,未限制内存,编译过程使用 Top 查看确实 20 核并发,Cpu 和 Mem没有达到瓶颈,iostat 看磁盘使用率每秒 60%」
  • 5 延伸阅读

  • 编写可读代码的艺术

    software Engineer at Google

    人月神话

    数据密集型应用系统设计

CSAPP Linking 笔记

符号和符号表

static属性声明的全局变量/函数都是模块私有的,任意不被static修饰的全局变量/函数都是公共的,可以被其他模块访问

因此,在每个可重定位目标模块中存在三类符号

  • 自己定义的全局符号
  • 由其他模块定义并由该模块引用的全局符号
  • 自己定义的局部符号–static修饰的函数/全局变量

符号解析

编译器解析符号引用的办法是将每个与它输入的可重定位目标文件中的一个确定的符号定义关联起来

如何解析多重定义的全局符号

区别强弱定义: (我的理解) 强定义是全局定义后有初始值,弱定义没有

  1. 不允许有多个同名的强符号
  2. 如果有一个强符号和多个弱符号同名,则选择强符号
  3. 如果多个弱符号同名,则随机选择一个

CSAPP 存储器层次结构 笔记

6.1 存储技术

  • 随机访问存储器 RAM

    • 静态 SRAM

      位存储模式:双稳态存储器单元

      只要有电,就能永远保持值

    • 动态 DRAM

      位存储模式: 电容充电

    • 比较

    • 每位晶体管数相对访问时间持续?敏感相对花费应用
      SRAM611000高速缓冲存储器
      DRAM1101主存/帧缓冲区
    • 非易失性存储器 ROM

      关电后仍然能保持信息

    • 访问主存

      通过数据总线和主存信息交互

  • 磁盘存储

    • 从磁盘读数据的效率是从DRAM读数据的几乎10万倍慢,不过ssd会快的多,不过只是相对于传统磁盘而言

    • 固态磁盘SSD

      • 读SSD比写要快

        • 因为随机写要擦除块,这个动作是毫秒级的
        • 如果试图写一个有数据的块,会先将这块的数据复制到另一个没数据的地方
      • SSD多次重复写后会损坏

6.2局部性

局部性原理

计算机程序倾向于引用邻近与其他最近引用过的数据项的数据项

简单原则

  • 重复引用相同变量的程序有良好的时间局部性
  • 对于步长为k的引用模式的程序,步长越小,空间局部性越好.
  • 对于取指令来说,循环有好的时间和空间局部性.循环体越小,迭代次数越多,局部性越好

6.3 存储器层次结构

  • L0 寄存器 –保存着从高速缓存中取出的字
  • L1 高速缓存 SRAM –缓存着从L2取出的缓存行 – 速度接近寄存器
  • L2 高速缓存 SRAM –缓存着从L3取出的缓存行 – 速度比L1慢
  • L3 高速缓存 SRAM –缓存着从主存高速缓存取出的缓存行 –速度比L2慢
  • L4 主存 DRAM – 保存着从本地磁盘取出的代码块
  • L5 本地耳机存储 本地磁盘 –保存着从远程网络服务器磁盘上读取的文件
  • L6 远程二级存储 [分布式文件系统,Web服务器]

6.4 高速缓存存储器

6.4.1 通用的高速缓存存储器组织结构

存储器地址位数: m

存储器地址数: M=2^m

高速缓存组数 S= 2^s

高速缓存组内缓存行: E

缓存行内数据块 B=2^b

高速缓存大小: C=S*E*B

6.4.2 直接映射高速缓存

特征: 每个组只有一行,因此字选择时简单,但容易发生抖动

流程: 假设执行一条读内存字w的指令

组选择:

  • 从w中抽取s个组标记位,s由高速缓存组数决定
  • 之后查看高速缓存中是否存在该组,如果存在就得到一个缓存命中,不存在就是缓存不命中

字选择:

  • 高速缓存中的偏移位标识了字节在块中的偏移

行替换:

  • 如果缓存不命中,就需要从层次结构的下一层中取出被请求的块,然后将心的块存储在组索引所示的块中

示例:

  • 书P429

冲突不命中:

  • 由直接映射的设计可以看出,如果程序访问大小为2的幂的数组,很可能会发生冲突不命中.
  • 相同组映射的内存块会不断的来回覆盖–抖动

6.4.3 组相联高速缓存

每个组都有 1<E<C/B 个高速缓存行的 的高速缓存通常称为E路组相联高速缓存

如果E=C/B 称为全相联高速缓存

行匹配

  • 检查多个行的标记和有效位,判断是否在缓存中

行替换

  • 如果组中有空行,则换到空行上去
  • 如果没有,则根据替换策略替换–比如LRU

6.4.4 写回

怎么更新层次结构中,低一层的副本

  1. 直写 – write throuth
    • 立即将w的高速缓存块写回到紧接着的第一层
    • 每次写都会引起总线流量
    • 能够使用独立于高速缓存的写缓冲区用来更新内存
    • 读不命中开销小
  2. 写回[ 延迟更新] write back
    • 只有当替换算法要驱逐这个块时,将这个块写到低一层
    • 显著减少总线流量
    • 增加复杂性– 需要维护一个新的位[修改位]
    • 允许更多到内存的贷款用于执行DMA的I/O.

如何处理写不命中

就是说,要写的块拿不到

  1. 写分配 write-allocate
    • 加载相应的低一层的块到高速缓存中,然后更新这个高速缓存块
    • 写回高速缓存通常是写分配的
  2. 非写分配 not-write-allocate
    • 避开高速缓存,直接把这个字写到低一层中
    • 直写高速缓存通常是非写分配的

6.4.7 性能影响

  1. 不命中率
    • 执行期间,内存引用不命中的比率 不命中数量/引用数量
  2. 命中时间
    • 从高速缓存传送一个字到CPU所需的时间
  3. 不命中处罚
    • 不命中所需的额外时间

下列因素提高的影响

  • 高速缓存:命中率提高,命中时间提高
  • 块大小:命中率提高,不命中处罚提高,损害时间局部性比空间局部性更好的程序的命中率
  • 相联度:降低抖动可能,成本提高,增加不命中处罚

越往层次下面走,传送时间增加,减少传送的数量就更为重要.

6.5 编写高速缓存友好的代码

两个原则

  • 让最常见的情况运行的最快
  • 尽量减少,每个循环内部的缓存不命中数量.

CSAPP Optimize 笔记

5.1 编译器优化能力的局限性

  • 编译器只做安全的优化–优化后和未优化的版本有一样的行为

限制

  • 内存别名使用

编译器并不知道指针指向哪里,因此它必须假设指针可能指向同一个位置.

  • 函数调用

大多数编译器不会去判断一个函数是否有副作用,因此它们倾向于将函数的调用保持不变

内联

包含函数调用的代码可以用内联函数替换过程进行优化,就是将函数内部的执行步骤内联到一起

在gcc中,只尝试单文件的内联,不会尝试多文件的内联(比如一组函数在其他文件内的函数中被调用)

5.2 表示程序性能

标准:CPE

每元素的周期数

5.3 消除低效循环

例如 :可以拿出来的拿出来

5.4减少过程调用

例: 如果一个循环中不断从结构体中的列表中获取值,而同时,结构体会不断对列表中值的存在进行判断

则可在保证安全的情况下,直接获取结构体中的列表进行访问.

5.5消除不必要的内存引用

例如

1
2
3
4
5
6
7
8
9
{// for loop
*dest=*dest+1;
}
// 将dest这个不断访问内存的东西挪到寄存器里
val tmp=*dest;
{
tmp=tmp+1;
}
*dest=tmp

5.6 了解现代处理器

  • 延迟界限

    当一系列操作必须按照严格顺序执行时会碰到

    因为下一条指令开始前,这一条必须结束

    代码中的数据相关限制了处理器利用指令级并行的能力时会碰到延迟界限

  • 吞吐量界限

    是处理器功能单元的原始计算能力,是程序性能的终极限制

5.7 循环展开

  • 比如n长的循环,2个一组展开之后,循环长度/2,这种称之为1*2循环展开

对一个n长的循环,进行k级别的展开,就需要将上限设置为n-k+1,这样最大循环索引会小于n

5.8 提高并行性

5.8.1 多个累积变量

例: 见书P370

上面的例子称之为2*2循环展开

5.8.2重新结合变换

该例子为2*1a循环展开

// slow version

acc= (acc * data[i]) * data[i+1]

//fast version

acc = acc * ( data[i] * data[i+1])

slow版里,每次计算都需要等之前的计算结果出来之后才能继续进行

fast版里,data[i]*& data[i+1]不受约束,因此可以被CPU并行优化,故效率高

5.9 一些限制因素

书P378

5.9.1 寄存器溢出

当用到的临时变量过多,使得寄存器不够用的时候,会调用栈来存储这些变量,这会使得程序效率变低.

5.9.2 分支预测和预测错误处罚

预测错误会导致较大的错误处罚,那么有什么办法来保证这个处罚对程序效率影响较小呢

  1. 不过分关心可预测的分支

    例如,大部分结束循环的语句判断都是不结束,预测时一般都按照不结束来判断. 这时候只在最后一次会导致预测错误处罚.

  2. 书写适合使用条件传送实现的代码

    最好使用条件数据传送而非条件控制转移.

    数据传送示例:

    max=a>b?a:b;

    这种形式适合流水线并行操作.

5.10 应用 性能提高技术

  1. 高级设计: 为遇到的问题选择适当的算法和数据结构
  2. 基本编码原则. 避免限制优化的因素
    1. 消除连续的函数调用
      • 在可能时,将计算一道循环外
    2. 消除不必要的内存引用
      • 引入临时变量来保存中间结果,只有在最后的值计算出来时,才将结果存放到数组和全局变量中.
  3. 低级优化. 结构化代码以利用硬件性能
    1. 展开循环,降低开销
    2. 使用多个累计变量和重新结合技术,找到方法提高指令级并行
    3. 用功能性的风格重写条件操作,使编译采用条件数据传送.

CSAPP Machine Level Programming

C 语言 基础属性

数组的指针运算– 数组存储的是它的指针,其指针++ 会跳过存储数据量的位置(如 int a[10],a++ ,arr[a]会+4)

struct的对齐

由于会根据struct中的最大的基本结构类型[int,double,float 之类的,和列表没关系]来进行对齐[例如,struct中存在double就会按照8byte对齐,如果最大只有int,就按照4byte对齐],因此,最好将结构合理组织,

1
2
3
4
5
6
7
8
9
10
struct S4{//char :1 byte, int : 4byte
char c;// 产生3个用于对齐的内存浪费
int i;
char d;// 产生3个用于对齐的浪费
}// waste 3+3 byte
struct S5{
int i;
char c;
char d;//c,d一并存储,产生2个用于对齐的浪费
}

Memory Layout

IMG:

image-20221007163747876

Buffer OverFlow

image-20221007163839893
  • 注: 内存是按照0x7FFFFFFFFFFF 也就是2^47来作为地址的,所以各位置之间可能会有较大的差距[因为暂时,硬件条件并不会使得整块可供分配的内存id映射被用尽]

stack:

  • 8MB
  • 向下拓展[地址高标号低]

Data:

  • 用于存放程序开始时分配的数据
    • 存放全局变量

Heap:

  • 存放通过malloc/相关函数申请的变量,会动态变化
  • 大的数据块会出现在靠近stack的位置,并向下增长,小的数据块会出现在靠近Data的位置,并向上增长

SharedLibraries:

  • 存放库函数代码[一般在磁盘上]
  • 在运行时动态加载到内存中

Unions

BufferOverFlow

代码注入攻击详解

举例: gets 会不断读取字符串,直至收到一个’\0’

1
2
3
4
5
6
void echo()
{
char buf[4];/* Way too small*/
gets(buf);
puts(buf);
}

此时,若输入大于4个字符,echo还是可以接收

查看汇编代码可以看到,调用echo的时候给stackFrame分配了24byte的空间

如果输入大于23个字符,就会报出 segment fault

  • 这时候该函数的返回位置可能被溢出的字符串覆盖,使得函数不会到main这个接口,而是进入一个新的地区
  • 这就是代码注入攻击

小于23就没事,

How to Avoid

  1. 使用安全的替代
    1. fgets-> gets
    2. strncopy-> strcopy
    3. scanf(“%ns”)-> scanf(“%s”)
  2. Randomized stack offsets– 地址空间布局随机化
    1. 使得每次程序运行的时候,它分配到的缓冲区长度都是变化的
  3. None executable code segments
    1. 在可读/可写等内存标识之外增加一个 “execute” 权限
  4. stack canary 栈保护机制
    1. image-20221007201652095
    2. 程序会检测到栈溢出的问题并返回

A Skill to Avoid Randomized stack offset/None executable code segments

  • 但是躲不开canary

Gadget

A Example:image-20221008122557042

p后面的值恰好和mov rax rdi 相等,结尾又是一个c3,因此这一段额外的代码会在p赋值之后执行,并返回.

这样就实现了在代码中插入一定量的自己的小代码,返回后就可以通过获取rsp栈中的代码,来将之前的代码块拼接一起执行.

GDB Trick

disass [FUNC_NAME]

解析对应函数的汇编代码

论文阅读笔记-Towards a Fully Disaggregated and Programmable Data Center

Abstract

目的: 探索建立一个完全分布式的数据中心的可能性。

topLayer: 探索了两种抽象形式,并提出了一种原分布式的方法

bottomLayer: 描述了建立分布式设备与连接它们的网络基础设施所需的硬件和关键功能。

connection:提出了一个静态时间组价,它将不同的用户程序编译到异构的分布式设备中,通过一个disaggregation-native 的中间表示法。

同时提出了一个运行时的系统,他管理硬件资源,并安排编译器生成的执行单元。

Introduction

现有的问题:

应用的颗粒化和硬件性能增长速度的限制,对分布式数据中心提出了要求。

而现有的分布式解决方案无法对网络和计算器进行分布。

同时,现有的数据中心网络的设计目的是连接服务器,但是,怎么高效地连接分布式设备呢?

最后,目前仍不清楚如何将应用映射到一个分布式的硬件平台。

两种设想的抽象类型

  1. 向后兼容的抽象,
    用户不知道硬件性质,他们会认为程序在虚拟机上运行,完全与服务器无关。
  2. 向应用程序暴露部分分布式、可编程的底层性质。
    这种类型会有更好的 性能,因为用户可以直接控制并利用低层次的系统功能。如网络通信等。

实现应用的分布式映射

不同于以往的分布式架构,这里使用Intetmediate Representation 作为中层架构。它是围绕着分解执行单元的概念进行的。

用MLIR将程序分解成小编码块

如何在FDP-DC中建立硬件基础设施

提供了建立一个分布式设备的指导方针,并却行了它的三个方针,网络连接性,硬件虚拟化和多用户隔离

设想了一个可重配置的网络架构

runtime management system

FDP-DC OS 监督整个资源池,在规划的时候会采用编译器的提示

FDP-DC Design

image-20221002152443741