术语“CPU限制”和“I/O限制”是什么意思?


当前回答

多线程是最重要的

在这个回答中,我将研究区分CPU和IO限制工作的一个重要用例:在编写多线程代码时。

RAM I/O绑定示例:Vector Sum

考虑一个程序,它将单个向量的所有值相加:

#define SIZE 1000000000
unsigned int is[SIZE];
unsigned int sum = 0;
size_t i = 0;
for (i = 0; i < SIZE; i++)
    /* Each one of those requires a RAM access! */
    sum += is[i]

通过为每个核心平均分割数组来实现并行,在普通的现代台式机上用处有限。

例如,在我的Ubuntu 19.04上,联想ThinkPad P51笔记本电脑,CPU:英特尔酷睿i7-7820HQ CPU(4核/ 8线程),RAM: 2倍三星M471A2K43BB1-CRC(2倍16GiB),我得到的结果如下:

图数据。

请注意,运行之间有很多差异。但是我不能进一步增加数组的大小,因为我已经是8GiB了,而且我今天没有心情进行多次运行的统计。然而,在做了许多手动运行之后,这似乎是一个典型的运行。

基准测试代码:

POSIX C线程源代码中使用的图形。 下面是一个c++版本,可以产生类似的结果。 剧情脚本

我不知道足够的计算机架构来完全解释曲线的形状,但有一件事是明确的:由于我使用了所有的8个线程,计算并没有像天真地期望的那样快8倍!出于某种原因,2和3个线程是最优的,增加更多线程只会让事情变得更慢。

将其与CPU约束的工作进行比较,后者实际上要快8倍:在time(1)的输出中,'real', 'user'和'sys'是什么意思?

所有处理器共享一个连接到RAM的内存总线的原因:

CPU 1   --\    Bus    +-----+
CPU 2   ---\__________| RAM |
...     ---/          +-----+
CPU N   --/

所以内存总线很快成为瓶颈,而不是CPU。

这是因为两个数字相加只需要一个CPU周期,而内存读取在2016年的硬件中大约需要100个CPU周期。

因此,CPU对每个字节的输入数据所做的工作太小了,我们称之为io绑定进程。

进一步加速计算的唯一方法是使用新的内存硬件加速单个内存访问,例如多通道内存。

例如,升级到更快的CPU时钟并不是很有用。

其他的例子

matrix multiplication is CPU-bound on RAM and GPUs. The input contains: 2 * N**2 numbers, but: N ** 3 multiplications are done, and that is enough for parallelization to be worth it for practical large N. This is why parallel CPU matrix multiplication libraries like the following exist: http://www.netlib.org/scalapack/pblas_qref.html http://icl.cs.utk.edu/magma/software/ Cache usage makes a big difference to the speed of implementations. See for example this didactic GPU comparison example. See also: Why can GPU do matrix multiplication faster than CPU? BLAS equivalent of a LAPACK function for GPUs Networking is the prototypical IO-bound example. Even when we send a single byte of data, it still takes a large time to reach it's destination. Parallelizing small network requests like HTTP requests can offer a huge performance gains. If the network is already at full capacity (e.g. downloading a torrent), parallelization can still increase improve the latency (e.g. you can load a web page "at the same time"). A dummy C++ CPU bound operation that takes one number and crunches it a lot: serial parallel Sorting appears to be CPU based on the following experiment: Are C++17 Parallel Algorithms implemented already? which showed a 4x performance improvement for parallel sort, but I would like to have a more theoretical confirmation as well The well known Coremark benchmark from EEMBC explicitly checks how well a suite of problems scale. Sample benchmark result clearing showing that: Workload Name (iter/s) (iter/s) Scaling ----------------------------------------------- ---------- ---------- ---------- cjpeg-rose7-preset 526.32 178.57 2.95 core 7.39 2.16 3.42 linear_alg-mid-100x100-sp 684.93 238.10 2.88 loops-all-mid-10k-sp 27.65 7.80 3.54 nnet_test 32.79 10.57 3.10 parser-125k 71.43 25.00 2.86 radix2-big-64k 2320.19 623.44 3.72 sha-test 555.56 227.27 2.44 zip-test 363.64 166.67 2.18 MARK RESULTS TABLE Mark Name MultiCore SingleCore Scaling ----------------------------------------------- ---------- ---------- ---------- CoreMark-PRO 18743.79 6306.76 2.97 the linking of a C++ program can be parallelized to a certain degree: Can gcc use multiple cores when linking?

如何发现如果你是CPU或IO限制

非ram IO绑定像磁盘,网络:ps aux,然后检查CPU% / 100 < n线程。如果是,你是IO绑定,例如阻塞读取只是等待数据和调度器跳过该过程。然后使用sudo iotop等进一步的工具来确定哪个IO是问题所在。

或者,如果执行速度很快,并且对线程数量进行了参数化,那么对于CPU限制的工作,您可以很容易地从时间上看到性能随着线程数量的增加而提高:在time(1)的输出中,'real'、'user'和'sys'是什么意思?

RAM- io绑定:更难判断,因为RAM等待时间包含在CPU%测量中,请参见:

如何检查应用程序是否cpu或内存限制? https://askubuntu.com/questions/1540/how-can-i-find-out-if-a-process-is-cpu-memory-or-disk-bound

一些选项:

Intel Advisor rooline(非免费):https://software.intel.com/en-us/articles/intel-advisor-roofline(存档)“屋顶线图是与硬件限制相关的应用程序性能的可视化表示,包括内存带宽和计算峰值。”

GPUs

当您第一次将输入数据从常规CPU可读RAM传输到GPU时,GPU会遇到IO瓶颈。

因此,对于CPU受限的应用,gpu只能比CPU更好。

然而,一旦数据传输到GPU,它可以比CPU更快地对这些字节进行操作,因为GPU:

有更多的数据本地化比大多数CPU系统,所以数据可以更快地访问某些核心比其他 利用数据并行性并牺牲延迟,跳过尚未准备好立即操作的任何数据。 由于GPU必须处理大量的并行输入数据,因此最好跳过可能可用的下一个数据,而不是像CPU那样等待当前数据可用并阻塞所有其他操作

因此GPU可以比CPU更快,如果你的应用程序:

可以高度并行化:不同的数据块可以在同一时间彼此分开处理 每个输入字节需要足够多的操作(不像向量加法,每个字节只做一次加法) 有大量的输入字节

这些设计选择最初是针对3D渲染的应用,其主要步骤如OpenGL中的什么是shaders,我们需要它们做什么?

顶点着色器:用4x4矩阵乘以一堆1x4向量 片段着色器:计算一个三角形的每个像素的颜色基于它在三角形内的相对位置

因此我们得出结论,这些应用程序是cpu限制的。

随着可编程GPGPU的出现,我们可以观察到几个GPGPU应用程序作为CPU绑定操作的例子:

Image Processing with GLSL shaders? Local image processing operations such as a blur filter are highly parallel in nature. Is it possible to build a heatmap from point data at 60 times per second? Plotting of heatmap graphs if the plotted function is complex enough. https://www.youtube.com/watch?v=fE0P6H8eK4I "Real-Time Fluid Dynamics: CPU vs GPU" by Jesús Martín Berlanga Solving partial differential equations such as the Navier Stokes equation of fluid dynamics: highly parallel in nature, because each point only interacts with their neighbour there tend to be enough operations per byte

参见:

为什么我们还在使用cpu而不是gpu ? gpu的弱点是什么? https://www.youtube.com/watch?v=_cyVDoyI6NE“CPU vs GPU(有什么区别?)-计算机爱好者”

CPython全局解释器锁

作为一个快速的案例研究,我想指出Python全局解释器锁(GIL): CPython中的全局解释器锁(GIL)是什么?

这个CPython实现细节可以防止多个Python线程有效地使用cpu限制的工作。CPython文档说:

CPython implementation detail: In CPython, due to the Global Interpreter Lock, only one thread can execute Python code at once (even though certain performance-oriented libraries might overcome this limitation). If you want your application to make better use of the computational resources of multi-core machines, you are advised to use multiprocessing or concurrent.futures.ProcessPoolExecutor. However, threading is still an appropriate model if you want to run multiple I/O-bound tasks simultaneously.

因此,这里有一个例子,cpu绑定的内容不适合,而I/O绑定的内容适合。

JavaScript async和Node.js worker_threads

这种情况与Python类似。

JavaScript基本上是单线程的。不确定这是否是语言标准的一部分(对于Python来说,它不是,除了参考CPython实现AFAIK之外,甚至没有一个语言标准)。

JavaScript有async关键字,它允许执行暂停,然后它开始执行其他东西。你可以这样写:

async function myfunc(init) {
  ret = init
  for (let i = 0; i < 1000000; i++) {
    ret += i*i + 2*i + 3
  }
  return ret;
}

async function doNetworkRequest(init) {
  // Some native method that gets network data.
}

const [first, second, myfunc_ret1, myfunc_ret2] = await Promise.all([
  doNetworkRequest('example.com'),
  doNetworkRequest('example2.com'),
  myfunc(1),
  myfunc(2),
])

await表示“等待所有这些异步的事情在继续之前完成”。

然而,在CPU上一次只能运行一个异步方法,因此myfunc的CPU密集型工作根本不会因此而加速。

然而,网络访问的原型IO绑定操作可以得到加速,因为这将一个接一个地触发两个网络请求,并在服务器/网络完成工作时等待两者返回。

事实上,该语言中有一个专门用于此的关键字async,这说明:在浏览器主导的JavaScript上下文中,网络请求非常重要。

然而,随着Node.js的出现,人们开始越来越想要并行化CPU工作负载,他们达成了与CPython类似的解决方案:创建单独的进程而不是线程。这是通过worker_threads库完成的,它:

实际上以JavaScript脚本路径作为输入,并启动一个新的解释器实例来运行它 由于进程不共享内存空间,因此可以使用消息传递系统在实例之间传递消息

https://medium.com/@Trott/ using-work-threads-in -node-js-80494136dbb6包含一个很好的例子。

worker_threads的文档再次说明了这个答案中其他地方提到的差异:

worker(线程)对于执行cpu密集型JavaScript操作非常有用。它们对I/ o密集型工作没有太大帮助。Node.js内置的异步I/O操作比Workers更高效。

在浏览器上也有Web Workers,不知道它与Node的实现有什么区别:Web Workers和Worker Threads之间有什么区别?

其他回答

I/O限制是指完成计算所需的时间主要由等待输入/输出操作完成的时间决定的一种情况。

这与受CPU限制的任务相反。当请求数据的速度比消耗数据的速度慢时,或者换句话说,花费在请求数据上的时间比处理数据的时间多时,就会出现这种情况。

当一个应用程序在执行期间的算术/逻辑/浮点(A/L/FP)性能大部分接近处理器的理论峰值性能(数据由制造商提供,由处理器的特性决定:核数、频率、寄存器、alu、fpu等)时,它就被cpu绑定了。

peek性能在实际应用中是很难实现的,并不是说不可能。大多数应用程序在不同的执行过程中访问内存,处理器在几个周期内不会执行A/L/FP操作。由于内存和处理器之间存在距离,这被称为冯·诺依曼限制。

If you want to be near the CPU peak-performance a strategy could be to try to reuse most of the data in the cache memory in order to avoid requiring data from the main memory. An algorithm that exploits this feature is the matrix-matrix multiplication (if both matrices can be stored in the cache memory). This happens because if the matrices are size n x n then you need to do about 2 n^3 operations using only 2 n^2 FP numbers of data. On the other hand matrix addition, for example, is a less CPU-bound or a more memory-bound application than the matrix multiplication since it requires only n^2 FLOPs with the same data.

下图显示了在Intel i5-9300H中使用简单的矩阵加法和矩阵乘法算法获得的FLOPs:

注意,正如预期的那样,矩阵乘法的性能要大于矩阵加法。可以通过运行这个存储库中的test/gemm和test/matadd来重现这些结果。

我建议你也去看看J. Dongarra关于这个效果的视频。

这很直观:

如果一个程序在CPU更快的情况下运行得更快,那么它就受到了CPU的限制,也就是说,它的大部分时间都在简单地使用CPU(进行计算)。计算π的新数字的程序通常是cpu限制的,它只是处理数字。

如果一个程序能够在I/O子系统更快的情况下运行得更快,那么它就是I/O约束的。具体的I/O系统是不同的;我通常把它与磁盘联系在一起,当然,一般来说,网络或通信也很常见。在一个大文件中查找一些数据的程序可能会成为I/O限制,因为瓶颈是从磁盘读取数据(实际上,这个例子在今天可能有点过时,从ssd读取数百MB/s)。

I/O绑定进程:—如果一个进程生命周期的大部分时间都处于I/O状态,那么这个进程就是一个I/O绑定进程。例子:计算器,internet explorer

CPU绑定进程:—如果进程生命周期的大部分时间都花在CPU上,那么它就是CPU绑定进程。

CPU限制是指程序被CPU或中央处理器所限制,而I/O限制是指程序被I/O或输入/输出所限制,例如读写磁盘、网络等。

一般来说,在优化计算机程序时,人们试图找出瓶颈并消除它。知道您的程序受CPU限制是有帮助的,这样就不会不必要地优化其他东西。

[我所说的“瓶颈”是指使你的程序运行得比原本要慢的东西。]