Answers:
这很直观:
如果程序在CPU更快的情况下运行得更快,则该程序受CPU限制,即,程序大部分时间仅使用CPU(进行计算)。计算π的新数字的程序通常受CPU限制,只是计算数字。
如果I / O子系统更快,则程序运行得更快,则该程序受I / O约束。确切的I / O系统意味着什么,可能会有所不同。我通常将其与磁盘关联,但是当然,一般而言,联网或通信也很常见。通过大文件查找某些数据的程序可能会成为I / O绑定,因为瓶颈是然后从磁盘读取数据(实际上,此示例如今可能已经过时,具有数百MB / s的速度)。来自SSD)。
CPU绑定是指进程进行的速度受CPU速度的限制。在少量数字上执行计算(例如,将较小的矩阵相乘)的任务很可能受CPU限制。
I / O绑定是指进程进行的速率受I / O子系统的速度限制。处理磁盘数据的任务(例如,计算文件中的行数)很可能受I / O约束。
内存限制是指进程进行的速度受可用内存量和该内存访问速度的限制。处理大量内存数据(例如,乘以大型矩阵)的任务很可能是“内存绑定”。
高速缓存绑定是指进程进度受可用高速缓存的数量和速度限制的速率。仅处理超出缓存容量的更多数据的任务将受到缓存限制。
I / O界限将比内存界限慢,而缓存界限将比CPU界限慢。
受I / O约束的解决方案不一定获得更多的内存。在某些情况下,可以围绕I / O,内存或缓存限制来设计访问算法。请参阅缓存遗忘算法。
多线程
在这个答案中,我将研究一种区分CPU与IO受限工作的重要用例:编写多线程代码时。
RAM I / O绑定示例:矢量和
考虑一个对单个向量的所有值求和的程序:
#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上,带有CPU的Lenovo ThinkPad P51笔记本电脑:Intel Core i7-7820HQ CPU(4核/ 8线程),RAM:2x Samsung M471A2K43BB1-CRC(2x 16GiB)我得到如下结果:
绘制数据。
请注意,运行之间有很多差异。但是,由于我已经在8GiB上工作,因此无法进一步增加阵列大小,并且今天我不打算进行多次运行的统计。但是,在进行许多手动运行之后,这似乎是一种典型的运行。
基准代码:
我不了解足够的计算机体系结构来完全解释曲线的形状,但是有一点很清楚:由于我使用了所有8个线程,因此计算速度并没有像天真预期的那样快8倍!由于某些原因,2和3个线程是最佳的,添加更多线程只会使事情变慢。
将此与CPU限制的工作进行比较,它实际上的确快了8倍:time(1)输出中的“ real”,“ user”和“ sys”是什么意思?
这是所有处理器共享链接到RAM的单个内存总线的原因:
CPU 1 --\ Bus +-----+
CPU 2 ---\__________| RAM |
... ---/ +-----+
CPU N --/
因此内存总线很快成为瓶颈,而不是CPU。
发生这种情况是因为在2016年硬件中,将两个数字相加会占用一个CPU周期,而内存读取则需要约100个CPU周期。
因此,每字节输入数据完成的CPU工作量太小,我们将其称为IO绑定过程。
进一步加快计算速度的唯一方法是使用新的内存硬件(例如多通道内存)加快单个内存的访问。
例如,升级到更快的CPU时钟将不是很有用。
其他例子
矩阵乘法在RAM和GPU上受CPU限制。输入包含:
2 * N**2
数字,但是:
N ** 3
乘法完成了,对于实际的大N来说,这足以使并行化值得。
这就是为什么存在以下并行CPU矩阵乘法库的原因:
缓存的使用对实现速度有很大的影响。参见例如这个教学GPU比较示例。
也可以看看:
网络是典型的IO绑定示例。
即使我们发送单个字节的数据,也要花费大量时间才能到达目的地。
并行处理小型网络请求(例如HTTP请求)可以极大地提高性能。
如果网络已经满负荷运行(例如下载torrent),则并行化仍会增加延迟时间(例如,您可以“同时”加载网页)。
一个虚假的C ++ CPU绑定操作,取一个数字并对其进行大量运算:
根据以下实验,排序似乎是CPU:是否已实现C ++ 17并行算法?该结果显示并行排序的性能提高了4倍,但我也想获得更多理论上的确认
如何找出您是否受CPU或IO约束
将非RAM IO绑定为磁盘,网络:ps aux
,则为CPU% / 100 < n threads
。如果是,则您受IO限制,例如,阻塞read
s在等待数据,而调度程序正在跳过该过程。然后使用其他工具sudo iotop
来确定确切的问题是哪个IO。
或者,如果执行速度很快,并且您对线程数进行了参数化,则可以很容易地看到,time
随着CPU绑定工作的线程数增加,性能会提高:“ real”,“ user”和“ sys”分别是什么意思time(1)的输出?
RAM-IO限制:很难说,因为RAM等待时间包含在CPU%
测量中,另请参见:
一些选项:
GPU
首次将输入数据从常规CPU可读RAM传输到GPU时,GPU会有IO瓶颈。
因此,对于受CPU约束的应用程序,GPU只能比CPU更好。
但是,一旦将数据传输到GPU,由于GPU:
比大多数CPU系统具有更多的数据本地化,因此某些内核可以比其他内核更快地访问数据
通过跳过所有尚未准备立即操作的数据来利用数据并行性并牺牲延迟。
由于GPU必须对大型并行输入数据进行操作,因此最好跳过可能可用的下一个数据,而不是等待当前数据可用并阻塞所有其他操作,例如CPU
因此,如果您的应用程序使用以下处理器,则GPU可以比CPU更快。
这些设计选择最初是针对3D渲染的应用程序,其主要步骤如OpenGL中的着色器所示,我们需要它们做什么?
因此我们得出结论,这些应用程序受CPU限制。
随着可编程GPGPU的问世,我们可以观察到几种GPGPU应用程序,它们是CPU绑定操作的示例:
本质上,诸如模糊滤波器之类的局部图像处理操作是高度并行的。
如果绘制的函数足够复杂,则绘制热图图。
https://www.youtube.com/watch?v=fE0P6H8eK4I “实时流体动力学:CPU与GPU”
求解偏微分方程,例如流体动力学的Navier Stokes方程:
也可以看看:
CPython全局解释器锁(GIL)
作为快速案例研究,我想指出一下Python全局解释器锁(GIL):CPython中的全局解释器锁(GIL)是什么?
这个CPython实现细节阻止了多个Python线程有效地使用CPU绑定的工作。在CPython的文档说:
CPython实现细节:在CPython中,由于使用了全局解释器锁,因此只有一个线程可以一次执行Python代码(即使某些面向性能的库可能克服了此限制)。如果您希望应用程序更好地利用多核计算机的计算资源,建议使用
multiprocessing
或concurrent.futures.ProcessPoolExecutor
。但是,如果您要同时运行多个I / O绑定任务,则线程化仍然是合适的模型。
因此,这里有一个示例,其中CPU绑定的内容不适合,而I / O绑定的则不适合。
I / O界限是指一种条件,其中完成计算所需的时间主要由等待输入/输出操作完成所花费的时间来确定。
这与受CPU约束的任务相反。当请求数据的速率比消耗数据的速率慢,或者换句话说,请求数据花费的时间多于处理数据的时间时,就会出现这种情况。
异步编程的核心是Task和Task对象,它们对异步操作进行建模。它们由async和await关键字支持。在大多数情况下,该模型非常简单:
对于绑定到I / O的代码,您需要等待一个操作,该操作将在异步方法内部返回Task或Task。
对于受CPU约束的代码,您需要等待通过Task.Run方法在后台线程上启动的操作。
关键字await是魔术发生的地方。它将控制权交给执行等待的方法的调用者,最终允许UI响应或服务具有弹性。
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);
};
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);
};
上面的示例显示了如何使用异步和等待来进行I / O绑定和CPU绑定的工作。您可以确定何时需要做的工作是受I / O约束或受CPU约束的关键,因为它会极大地影响代码的性能,并可能导致滥用某些构造。
在编写任何代码之前,您应该问两个问题:
您的代码会“等待”某事,例如数据库中的数据吗?
- 如果您的回答是“是”,则您的工作受I / O约束。
您的代码会执行非常昂贵的计算吗?
- 如果回答“是”,则说明您的工作受CPU限制。
如果您的工作受I / O限制,请使用async并等待而无需 Task.Run。您不应该使用任务并行库。深度异步中概述了这样做的原因文章中。
如果您的工作受CPU限制并且您关心响应性,请使用async和await,但使用Task.Run在另一个线程上启动工作。如果该工作适合于并发性和并行性,则还应该考虑使用Task Parallel Library。
当执行期间的算术/逻辑/浮点(A / L / FP)性能大部分接近处理器的理论峰值性能时(该数据由制造商提供,并由处理器的特性确定),该应用程序受CPU限制。处理器:内核数,频率,寄存器,ALU,FPU等)。
要说真实的性能很难在现实世界中的应用程序中实现,这并不是说不可能。大多数应用程序在执行的不同部分访问内存,并且处理器在几个周期内未执行A / L / FP操作。由于内存和处理器之间存在距离,因此这称为冯·诺依曼限制。
如果要接近CPU峰值性能,则可以尝试重用高速缓存中的大多数数据,以避免需要来自主内存的数据。利用此功能的算法是矩阵矩阵乘法(如果两个矩阵都可以存储在高速缓存中)。发生这种情况的原因是,如果矩阵是大小,n x n
那么您只需要2 n^3
使用2 n^2
FP数量的数据就可以进行操作。另一方面,例如,与矩阵乘法相比,矩阵加法是CPU限制较少的应用程序,或者是存储器限制较大的应用程序,因为它仅需要矩阵乘法n^2
具有相同数据的FLOP。
下图显示了使用天真算法在英特尔i5-9300H中用于矩阵加法和矩阵乘法的FLOP:
注意,正如预期的那样,矩阵乘法的性能大于矩阵加法的性能。这些结果可以通过运行来复制test/gemm
并test/matadd
在此存储库中可用。
我还建议您观看J. Dongarra提供的有关此效果的视频。
I / O绑定进程:-如果进程生命周期的大部分时间都处于I / O状态,则该进程就是AI / O绑定进程。例如:-calculator,internet Explorer
CPU绑定进程:-如果进程寿命的大部分时间都花在cpu上,那么它就是cpu绑定进程。