“CPU边界”这个术语是什么意思?和“I/O bound”;的意思吗?

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

302369 次浏览

这很直观:

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

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

CPU绑定意味着程序被CPU或中央处理器阻塞,而I / O绑定意味着程序被I/O或输入/输出阻塞,例如对磁盘、网络等的读写。

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

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

当你的程序等待I / O(即。磁盘读/写或网络读/写等),即使程序停止,CPU也可以自由地执行其他任务。程序的速度主要取决于IO发生的速度,如果你想加快速度,就需要加快I/O。

如果你的程序正在运行大量的程序指令而不等待I/O,那么它就被称为CPU限制。加速CPU将使程序运行得更快。

在任何一种情况下,加速程序的关键可能不是加快硬件,而是优化程序以减少所需的IO或CPU数量,或者让它在执行CPU密集型操作的同时执行I/O。

另一种表达相同想法的方式是:

  • 如果加速CPU并没有加速你的程序,它可能是I/O绑定。

  • 如果加速I/O(例如使用更快的磁盘)没有帮助,那么您的程序可能是CPU受限的。

(我使用“可能是”是因为你需要考虑其他资源。内存就是一个例子。)

CPU绑定表示进程的速度受CPU速度的限制。对一小组数字执行计算的任务,例如乘小矩阵,可能会受到CPU的限制。

I / O绑定表示进程进程的速度受I/O子系统的速度限制。处理来自磁盘的数据的任务(例如,计算文件中的行数)可能受到I/O限制。

内存约束表示进程进行的速度受可用内存数量和内存访问速度的限制。处理大量内存内数据的任务,例如乘法大型矩阵,很可能是memory Bound。

缓存绑定表示进程进程受可用缓存的数量和速度限制的速率。如果一个任务处理的数据超过了缓存的容量,那么它就会被缓存绑定。

I/O绑定比内存绑定慢,缓存绑定比CPU绑定慢。

I/O受限的解决方案不一定是获得更多内存。在某些情况下,访问算法可以围绕I/O、内存或缓存限制进行设计。看到缓参无关算法

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

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

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

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

IO绑定进程:花更多的时间做IO比计算,有很多 短CPU突发。 CPU绑定进程:花费更多的时间进行计算,很少有很长的CPU爆发

多线程是最重要的

在这个回答中,我将研究区分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了,而且我今天没有心情进行多次运行的统计。然而,在做了许多手动运行之后,这似乎是一个典型的运行。

基准测试代码:

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

将此与CPU约束的工作进行比较,后者实际上要快8倍:什么'real'和& # 39;sys # 39;时间(1)输出中的均值?

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

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

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

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

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

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

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

其他的例子

  • 矩阵乘法在RAM和gpu上是cpu绑定的。输入包含:

    2 * N**2
    

    数字,但是:

    N ** 3
    

    已经完成了乘法运算,这就足以让并行化在实际的大N中是值得的。

    这就是为什么存在如下这样的并行CPU矩阵乘法库:

    缓存的使用对实现的速度有很大的影响。例如this 说教性的GPU比较例子

    参见:

  • 网络是典型的io绑定示例。

    即使我们发送了一个字节的数据,它仍然需要很长时间才能到达目的地。

    并行小型网络请求(如HTTP请求)可以提供巨大的性能提升。

    如果网络已经满负荷(例如下载一个torrent),并行化仍然可以增加和改善延迟(例如你可以“同时”加载一个网页)。

  • 一个虚拟的c++ CPU绑定操作,取一个数字并大量处理它:

  • 排序似乎是基于以下实验的CPU: c++ 17并行算法已经实现了吗?,它显示了并行排序的4倍性能改进,但我也想有一个更理论性的确认

  • 来自EEMBC的著名的Coremark基准显式检查一组问题的扩展情况。基准测试结果清理示例显示:

    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
    
  • c++程序的链接可以并行到一定程度

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

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

或者,如果执行速度很快,并且你参数化了线程的数量,你可以很容易地从time中看到,对于CPU绑定的工作,随着线程数量的增加,性能会提高:什么'real'和& # 39;sys # 39;时间(1)输出中的均值?

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

一些选项:

gpu

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

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

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

  • 比大多数CPU系统有更多的数据本地化,因此某些核的数据访问速度比其他核快

  • 利用了数据并行性,通过跳过尚未准备好立即进行操作的任何数据而牺牲了延迟。

    由于GPU必须处理大量的并行输入数据,因此最好跳过可能可用的下一个数据,而不是像CPU那样等待当前数据可用并阻塞所有其他操作

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

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

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

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

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

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

参见:

CPython全局解释器锁

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

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

CPython实现细节:在CPython中,由于全局解释器锁,一次只能有一个线程执行Python代码(尽管某些面向性能的库可能会克服这一限制)。如果你想让你的应用程序更好地利用多核机器的计算资源,建议使用multiprocessingconcurrent.futures.ProcessPoolExecutor。但是,如果希望同时运行多个I/ o绑定任务,线程仍然是一种合适的模型。

因此,这里有一个例子,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上一次只能运行一个async方法,所以myfunc的CPU密集型工作根本不会因此而加速。

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

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

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

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

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

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

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

在浏览器中还有Web Workers,不确定它与Node的实现:Web Workers和Worker Threads之间的区别是什么?相比如何

看看微软怎么说。

异步编程的核心是Task和Task对象 建模异步操作。它们由async和支持 等待关键词。在大多数情况下,模型相当简单:

  • 对于I/ o绑定代码,您等待返回Task或

  • 对于cpu绑定代码,您等待在a上启动的操作 Task的后台线程。李运行方法。< / p > < / >

await关键字是魔术发生的地方。它将控制权交给 方法的调用者,它最终允许

. UI需要响应,或者服务需要弹性

I/ o绑定示例:从web服务下载数据

private readonly HttpClient _httpClient = new HttpClient();


downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await _httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};

cpu限制示例:为一个游戏执行一个计算 . bb0

private DamageResult CalculateDamageDone()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
}


calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work.  The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
上面的例子展示了如何使用async和 等待I/ o限制和cpu限制的工作。关键是你能识别出来 当你需要做的工作是I/ o限制或cpu限制时,因为它可以 极大地影响代码的性能,并可能导致

在你编写任何代码之前,你都应该问自己两个问题:

你的代码是否在“等待”一些东西,比如来自a的数据 数据库?< /强> < / p >

  • 如果你的回答是“是”,那么你的工作就是I/ o型的。

您的代码是否会执行非常昂贵的计算?

  • 如果您的回答是“是”,那么您的工作就受cpu限制。

如果你的工作是I/ o限制的,使用async和await without 的任务。运行。您不应该使用任务并行库。原因是 这在深度异步文章中概述

如果你的工作是cpu限制的,你关心的是响应能力, 使用async和await,但将工作派生到另一个线程 Task.Run。如果工作适合并发性和并行性, 你也应该考虑使用任务并行库.

.

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

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

如果您想要接近CPU的峰值性能,一种策略是尝试重用缓存中的大部分数据,以避免需要来自主存的数据。利用这一特性的算法是矩阵-矩阵乘法(如果两个矩阵都可以存储在缓存内存中)。这是因为如果矩阵的大小是n x n,那么你只需要使用2 n^2 FP数量的数据来执行大约2 n^3操作。另一方面,例如,矩阵加法是一个比矩阵乘法更少cpu或内存约束的应用程序,因为它只需要n^2 flop对相同的数据。

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

FLOPs比较矩阵加法和矩阵乘法

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

我还建议查看J. Dongarra给出的关于此效果的视频