程序员的自我修养笔记(一)

142 阅读6分钟

前言

由于本人在深入研究 swift 语言的某些底层机制,以及性能优化相关的事情。在这期间看了很多人的博客也实践了很多,但是终究感觉缺少理论基础,做的东西是一个个孤零零的点,而不是一张网,所以利用这本书把之前的实践和理论结合织成一张网。

这本书的开篇就说到,不要等到用的时候再读,我很赞同,但很不幸的是以前的我并没有感受到他们的用处在哪里,再加上书本本身又太专业(枯燥),理论较多,故一直搁置了。但是从另一方面来说,学习最好的时间是十年前,其实是现在。(书到用时方恨少啊。)

其实我们平时在开发中提到的性能优化(编译优化、启动优化、内存优化、包大小优化),其实在这本书中都能找到理论基础。相信上面的学会了,再去看看计算机图形学、计算机网络等,找到渲染优化的、网络优化的理论依据也不会是难事。

flag: 在学习的过程中,我会定期更新笔记。也会把相关的实践放在 github 上。(swift 版本)。

从 Hello World 说起

#include <stdio.h>

int main() {
    printf("Hello World\n");
    return 0;
}

对于下面的这些问题,你的脑子里面能够马上瓜出一个很清晰的又很明确的答案吗?

  • 程序为什么要被编译器编译了之后才可以运行?
  • 编译器在把 C 语言程序转换成可以执行的机器码的过程中做了什么,怎么做的?
  • 最后编译出来的可执行文件里面是什么?除了机器码还有什么?它们是怎么存放的,是怎么组织的?
  • #include<stdio.h> 是什么意思?把 stdio.h 包含进来意味着什么?C 语言库又是什么?它是怎么实现的?
  • 不同的编译器和不同的硬件平台以及不同的操作系统,最终编译出来的结果是一样吗?为什么?
  • Hello World 程序是怎么运行起来的?操作系统是怎么装载它的?它从哪儿开始执行,到哪儿结束?main 函数之前发生了什么?main 函数结束之后又发生了什么?
  • 如果没有操作系统,Hello World 可以运行吗?如果要在一台没有操作系统的机器上运行 Hello World 需要什么?应该怎么实现?
  • printf 是怎么实现的?它为什么可以有不定数量的参数?为什么它能够在终端上输出字符串?
  • Hello World 程序在运行时,它在内存是什么样子的?

上面的问题我做一下记录,希望这本书学完的时候,这些问题我都能很好的答出来。

万变不离其宗

计算机的三个核心部件:CPU、内存和I/O芯片。

早期的计算机由于没有显示设备,CPU 频率也不高,把他们都连在一个 BUS 上。为了 CPU 可以和 I/O 通信,每个设备都会有一个 I/O 控制器。

南桥: 连接所有的高速芯片,CPU、缓存、缓存等

北桥: 连接所有的低速芯片,硬盘、键盘、USB 等

计算机总线类型:

  • Cache bus
  • Local bus
  • Memory bus
  • PCI bus(系统总线)
  • ISA bus(低速设备总线)

smp 和多核

smp: 真多核。

多核: 共享比较昂贵的缓存部件,只保留多个核心。

站的高,望得远

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。

每一层之间通过下层提供的接口封装向上层提供更好的服务

API:应用程序接口,应用程序接口的提供者是运行库。

运行库:使用操作系统提供的系统调用接口,系统调用接口在实现中往往是以软件中断的方式提供。

操作系统做什么?

  1. 提供抽象的接口
  2. 管理硬件资源

不要让 CPU 打盹

多道程序 -> 分时系统 -> 多任务系统。

操作系统运行在一个受硬件保护的级别。所有的应用程序以进程的方式运行在比操作系统权限更低的级别。

设备驱动

硬件抽象成一系列的概念,真正可以和硬件打交道的就变成了硬件驱动程序,和操作系统运行在特权级,但又独立于操作系统。

内存不够怎么办?

如何将有限的内存资源分配给多个程序使用?

虚拟地址

增加一个中间层间接访问地址

分段

可以解决地址隔离和地址不确定的问题

分页

把地址空间人为地等分成固定大小的页。一般 4KB。

把常用的数据和代码装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它从磁盘中取出来。

保存也是页映射的目的之一,简单说就是可以设置每个页的权限属性,谁可以修改,谁可以访问。

映射是依靠硬件支持的。MMU(Memory Management Unit),一般集成在 CPU 内部。

线程基础

线程基础

什么是线程?

轻量级进程,程序执行流的最小单元。由线程 ID、当前指令指针(PC)、寄存器和堆栈组成。

各个线程之间共享程序的内存空间(包括代码段、数据段、堆等)及一些进程级资源(如打开文件的信号)。

优势

  • 某个操作可能会陷入长时间等待,等待的纯种会进入睡眠状态。
  • 可以一个负责交互,一个负责计算。
  • 可以支持并发操作
  • 可以发挥多核威力
  • 比多进程在数据共享方面效率要高很多

访问权限

私有

  • TLS
  • 寄存器(包括PC寄存器) 公有
  • 全局变量
  • 静态变量
  • 代码
  • 打开的文件

线程调度与优先级

  • 饿死现象(优先级反转)

可抢占线程和不可抢占线程 由于效率问题,现在基本都是可抢占线程了。

线程安全

竞争与原子操作

同步与锁

  • 二元信号量
  • 互斥量
  • 临界区
  • 读写锁
  • 条件变量

可重入与线程安全

过度优化 CPU 动态调度,在执行程序的时候为了提高效率有可能交换指令的顺序。

  1. 问题1
x = y = 0;
Thread1 Thread2
x = 1;  y = 1;
r1 = y; r2 = x;

结果 r1、r2 是有可能同时为 0 的。

  1. 问题2(单例的 double-check)
    1. 分配内存
    2. 在内存的位置上调用构造函数
    3. 将内存的地址赋值给 p

由于2、3可以交换,所以可能出现 p != null 了,但对象并没有初始化完成。这个时候用起来就很危险了。

多线程内部情况

  1. 一对一模型
  2. 多对一模型
  3. 多对多模型