Blog

半加器

半加器 #

我们先来看看,我们人在计算加法的时候一般会怎么操作。二进制的加法和十进制没什么区别,所以我们一样可以用列竖式来计算。我们仍然是从右到左,一位一位进行计算,只是把从逢 10 进 1 变成逢 2 进 1。

你会发现,其实计算一位数的加法很简单。我们先就看最简单的个位数。输入一共是 4 种组合,00、01、10、11。得到的结果,也不复杂。

一方面,我们需要知道,加法计算之后的个位是什么,在输入的两位是 00 和 11 的情况下,对应的输出都应该是 0;在输入的两位是 10 和 01 的情况下,输出都是 1。结果你会发现,这个输入和输出的对应关系,其实就是“异或门(XOR)”。

讲与、或、非门的时候,我们很容易就能和程序里面的“AND(通常是 & 符号)”“ OR(通常是 | 符号)”和“ NOT(通常是 ! 符号)”对应起来。可能你没有想过,为什么我们会需要“异或(XOR)”,这样一个在逻辑运算里面没有出现的形式,作为一个基本电路。其实,异或门就是一个最简单的整数加法,所需要使用的基本门电路。

算完个位的输出还不算完,输入的两位都是 11 的时候,我们还需要向更左侧的一位进行进位。那这个就对应一个与门,也就是有且只有在加数和被加数都是 1 的时候,我们的进位才会是 1。

所以,通过一个异或门计算出个位,通过一个与门计算出是否进位,我们就通过电路算出了一个一位数的加法。于是,我们把两个门电路打包,给它取一个名字,就叫作半加器(Half Adder)。 半加器的电路演示

Viewpoint #

From #

门电路标识

门电路标识 #

下面这些是门电路的标识,其他的电路都是由这些门电路组合起来的。 这些基本的门电路,是我们计算机硬件端的最基本的“积木”,就好像乐高积木里面最简单的小方块。看似不起眼,但是把它们组合起来,最终可以搭出一个星球大战里面千年隼这样的大玩意儿。我们今天包含十亿级别晶体管的现代 CPU,都是由这样一个一个的门电路组合而成的。

Viewpoint #

From #

自定义类型与类型别名

自定义类型与类型别名 #

如果我们要通过 Go 提供的类型定义语法,来创建自定义的数值类型,我们可以通过 type 关键字基于原生数值类型来声明一个新类型。

下面我们就来建立一个名为 MyInt 的新的数值类型看看:

type MyInt int32

这里,因为 MyInt 类型的底层类型是 int32,所以它的数值性质与 int32 完全相同,但它们仍然是完全不同的两种类型。根据 Go 的类型安全规则,我们无法直接让它们相互赋值,或者是把它们放在同一个运算中直接计算,这样编译器就会报错。

var m int = 5
var n int32 = 6
var a MyInt = m // 错误:在赋值中不能将m(int类型)作为MyInt类型使用
var a MyInt = n // 错误:在赋值中不能将n(int32类型)作为MyInt类型使用

要避免这个错误,我们需要借助显式转型,让赋值操作符左右两边的操作数保持类型一致,像下面代码中这样做:

var m int = 5
var n int32 = 6
var a MyInt = MyInt(m) // ok
var a MyInt = MyInt(n) // ok

我们也可以通过 Go 提供的类型别名(Type Alias)语法来自定义数值类型。和上面使用标准 type 语法的定义不同的是,通过类型别名语法定义的新类型与原类型别无二致,可以完全相互替代。我们来看下面代码:

type MyInt = int32
var n int32 = 6
var a MyInt = n // ok

你可以看到,通过类型别名定义的 MyInt 与 int32 完全等价,所以这个时候两种类型就是同一种类型,不再需要显式转型,就可以相互赋值。

Viewpoint #

From #

float32精度不足

float32精度不足 #

因为 float32 精度不足,导致输出结果与常识不符。比如下面这个例子就是这样,f1 与 f2 两个浮点类型变量被两个不同的浮点字面值初始化,但逻辑比较的结果却是两个变量的值相等。

var f1 float32 = 16777216.0
var f2 float32 = 16777217.0
fmt.Println(f1 == f2) // true

Viewpoint #

From #

浮点数在二进制中的表示

浮点数在二进制中的表示 #

浮点数在内存中的二进制表示(Bit Representation)要比整型复杂得多,IEEE 754 规范给出了在内存中存储和表示一个浮点数的标准形式,见下图: 我们看到浮点数在内存中的二进制表示分三个部分:符号位、阶码(即经过换算的指数),以及尾数。这样表示的一个浮点数,它的值等于: 其中浮点值的符号由符号位决定:当符号位为 1 时,浮点值为负值;当符号位为 0 时,浮点值为正值。公式中 offset 被称为阶码偏移值。

我们首先来看单精度(float32)与双精度(float64)浮点数在阶码和尾数上的不同。这两种浮点数的阶码与尾数所使用的位数是不一样的,你可以看下 IEEE 754 标准中单精度和双精度浮点数的各个部分的长度规定: 我们看到,单精度浮点类型(float32)为符号位分配了 1 个 bit,为阶码分配了 8 个 bit,剩下的 23 个 bit 分给了尾数。而双精度浮点类型,除了符号位的长度与单精度一样之外,其余两个部分的长度都要远大于单精度浮点型,阶码可用的 bit 位数量为 11,尾数则更是拥有了 52 个 bit 位。

接着,我们再来看前面提到的“阶码偏移值”,我想用一个例子直观地让你感受一下。在这个例子中,我们来看看如何将一个十进制形式的浮点值 139.8125,转换为 IEEE 754 规定中的那种单精度二进制表示。

  1. 我们要把这个浮点数值的整数部分和小数 部分,分别转换为二进制形式(后缀 d 表示十进制数,后缀 b 表示二进制数):整数部分:139d => 10001011b;小数部分:0.8125d => 0.1101b(十进制小数转换为二进制可采用“乘 2 取整”的竖式计算)。这样,原浮点值 139.8125d 进行二进制转换后,就变成 10001011.1101b。
  2. 移动小数点,直到整数部分仅有一个 1,也就是 10001011.1101b => 1.00010111101b。我们看到,为了整数部分仅保留一个 1,小数点向左移了 7 位,这样指数就为 7,尾数为 00010111101b。
  3. 计算阶码。 IEEE754 规定不能将小数点移动得到的指数,直接填到阶码部分,指数到阶码还需要一个转换过程。对于 float32 的单精度浮点数而言,阶码 = 指数 + 偏移值。偏移值的计算公式为 2^(e-1)-1,其中 e 为阶码部分的 bit 位数,这里为 8,于是单精度浮点数的阶码偏移值就为 2^(8-1)-1 = 127。这样在这个例子中,阶码 = 7 + 127 = 134d = 10000110b。float64 的双精度浮点数的阶码计算也是这样的。
  4. 将符号位、阶码和尾数填到各自位置,得到最终浮点数的二进制表示。尾数位数不足 23 位,可在后面补 0。

这样,最终浮点数 139.8125d 的二进制表示就为 0b_0_10000110_00010111101_000000000000。

...

加载GDTR

Content #

GDTR格式 在selector能够访问GDT之前,需要用lgdt指令加载GDTR。

lgdt [GDT_POINTER]
GDT_POINTER:   dw 3ffh  ;GDT limit
               dd 200000h ;GDT base

使用selector访问GDT时,处理器会检查是否会超出limit。当GDT的limit为0c6h时,下面的情形就超出limit范围。

mov ax, 0xc0    ;selector is c0h
mov ds, ax      ;GP 异常(超限)

GDT中segment descriptor是8个字节,这个selector需要访问的空间是c0h-c7h,而c7h超出了c6h的限制,引发#GP异常。

Viewpoint #

From #

不滥用形容词

Content #

相信大部分人都见过:随着年轻劳动力外出,中国部分农村凋敝。如果是你,你会怎么描述?《寻路中国》的作者是怎么描述的?为什么说从这段描述中可以看出不滥用形容词,也能写出好文章来?

在中国的有些农村,人们似乎离去得十分匆忙。石磨翻覆在地,泥土地板上散落着垃圾,
房屋只剩下框架,与沉寂的墓碑比肩而立。土墙已经开始剥落,
每当我看见这些空荡荡的村落,就会对自己说:来晚了。

这段话你看到了几个形容词?只有沉寂和空荡荡两个形容词,其它几乎都是名词、动词。尽管如此,村庄破败的样子也已经跃然纸上,而且印象深刻。这是为什么呢?

因为作者找准了意象,这就像元代戏曲家马致远写的秋思,“枯藤老树昏鸦,小桥流水人家,古道西风瘦马”。九个名词性词组就把一副萧瑟的秋景图摆在了我们面前。

Viewpoint #

From #

同步原语的三种常见类型

Content #

同步原语(synchronization primitive)监管内存的访问,与交通灯控制十字路口的访问方式相同,正如红绿灯一样,它们停止交通的流动,引起等待时间(延时)。常见的有哪三种类型?

  • mutex(MUTually EX clusive)锁只有锁持有者才能操作,其他线程会阻塞并等待CPU。
  • 自旋锁自旋锁允许锁持有者操作,其他的需要自旋锁的线程会在CPU 上循环自旋,检查锁是否被释放。虽然这样可以提供低延时的访问,被阻塞的线程不会离开 CPU,时刻准备着运行直到锁可用,但是线程自旋、等待也是对CPU 资源的浪费。
  • 读写锁读/写锁通过允许多个读者或者只允许一个写者而没有读者,来保证数据的完整性。

Viewpoint #

From #

列转行模式

Content #

有电影上映信息的文档形式如下:

{
    title: "Dunkirk",
    ...
    release_USA: "2017/07/23",
    release_UK: "2017/08/01",
    release_France: "2017/08/01",
    release_Festival_San_Jose:
    "2017/07/22"
}

需要针对每个地区及时间的索引,这会导致索引的数量非常大。这时,如果文档本身的数量也很大的话,维护的成本就会很高。MongoDB中用哪种模式可以很好地解决这个问题?

使用列转行模式,将多个地区及时间放入一个数组中:

{
    title: "Dunkirk",
    ...
    releases: [
        {country: "USA", date: "2017/07/23"},
        {country: "UK", date: "2017/08/01"},
        ...
    ]
}

再创建如下索引:

db.movies.createIndex({releases.country:1, releases.date:1})

Viewpoint #

From #

删除Taint

Content #

Kubernetes删除键为"node-role.kubernetes.io/master"的Taint,应如何操作?

$ kubectl taint nodes --all node-role.kubernetes.io/master-

我们在“node-role.kubernetes.io/master”这个键后面加上了一个短横线“-”,这个格式就意味着移除所有以“node-role.kubernetes.io/master”为键的 Taint。

Viewpoint #

From #