Content #
SIMD,中文叫作单指令多数据流(Single Instruction Multiple Data)。
我们先来体会一下 SIMD 的性能到底怎么样。下面是两段示例程序,一段呢,是通过循环的方式,给一个 list 里面的每一个数加 1。另一段呢,是实现相同的功能,但是直接调用 NumPy 这个库的 add 方法。在统计两段程序的性能的时候,我直接调用了 Python 里面的 timeit 的库。
$ python
>>> import numpy as np
>>> import timeit
>>> a = list(range(1000))
>>> b = np.array(range(1000))
>>> timeit.timeit("[i + 1 for i in a]", setup="from __main__ import a", number=1000000)
32.82800309999993
>>> timeit.timeit("np.add(1, b)", setup="from __main__ import np, b", number=1000000)
0.9787889999997788
>>>
从两段程序的输出结果来看,你会发现,两个功能相同的代码性能有着巨大的差异,足足差出了 30 多倍。也难怪所有用 Python 讲解数据科学的教程里,往往在一开始就告诉你不要使用循环,而要把所有的计算都向量化(Vectorize)。
有些同学可能会猜测,是不是因为 Python 是一门解释性的语言,所以这个性能差异会那么大。第一段程序的循环的每一次操作都需要 Python 解释器来执行,而第二段的函数调用是一次调用编译好的原生代码,所以才会那么快。如果你这么想,不妨试试直接用 C 语言实现一下 1000 个元素的数组里面的每个数加 1。你会发现,即使是 C 语言编译出来的代码,还是远远低于 NumPy。原因就是, NumPy 直接用到了 SIMD 指令,能够并行进行向量的操作。
而前面使用循环来一步一步计算的算法呢,一般被称为 SISD,也就是单指令单数据(Single Instruction Single Data)的处理方式。如果你手头的是一个多核 CPU 呢,那么它同时处理多个指令的方式可以叫作 MIMD,也就是多指令多数据(Multiple Instruction Multiple Dataa)。
为什么 SIMD 指令能快那么多呢?这是因为,SIMD 在获取数据和执行指令的时候,都做到了并行。一方面,在从内存里面读取数据的时候,SIMD 是一次性读取多个数据。
就以我们上面的程序为例,数组里面的每一项都是一个 integer,也就是需要 4 Bytes 的内存空间。Intel 在引入 SSE 指令集的时候,在 CPU 里面添上了 8 个 128 Bits 的寄存器。128 Bits 也就是 16 Bytes ,也就是说,一个寄存器一次性可以加载 4 个整数。比起循环分别读取 4 次对应的数据,时间就省下来了。
在数据读取到了之后,在指令的执行层面,SIMD 也是可以并行进行的。4 个整数各自加 1,互相之前完全没有依赖,也就没有冒险问题需要处理。只要 CPU 里有足够多的功能单元,能够同时进行这些计算,这个加法就是 4 路同时并行的,自然也省下了时间。
所以,对于那些在计算层面存在大量“数据并行”(Data Parallelism)的计算中,使用 SIMD 是一个很划算的办法。在这个大量的“数据并行”,其实通常就是实践当中的向量运算或者矩阵运算。在实际的程序开发过程中,过去通常是在进行图片、视频、音频的处理。最近几年则通常是在进行各种机器学习算法的计算。
而基于 SIMD 的向量计算指令,也正是在 Intel 发布 Pentium 处理器的时候,被引入的指令集。当时的指令集叫作 MMX,也就是 Matrix Math eXtensions 的缩写,中文名字就是矩阵数学扩展。而 Pentium 处理器,也是 CPU 第一次有能力进行多媒体处理。这也正是拜 SIMD 和 MMX 所赐。
从 Pentium 时代开始,我们能在电脑上听 MP3、看 VCD 了,而不用专门去买一块“声霸卡”或者“显霸卡”了。没错,在那之前,在电脑上看 VCD,是需要专门买能够解码 VCD 的硬件插到电脑上去的。而到了今天,通过 GPU 快速发展起来的深度学习技术,也一样受益于 SIMD 这样的指令级并行方案