假设我有两个程序P1和P2执行相同的功能,但P1在运行时消耗的功率小于P2。什么是编码方面的最佳实践,可以帮助我编写像P1这样的低功耗(低功耗)程序?您可以假设C或任何其他流行语言。
我从节省电池的角度来看(例如,智能手机)。
答案 0 :(得分:2)
首先,让我们考虑一下现代CPU消耗功率的因素(最多或最少):
如果特定线程花了一些时间做某事,内核可能会提高CPU频率以确保平滑的性能,从而增加功耗。事实上,power consumption increases with CPU frequency - exponentially (!) so(PDF),所以减少尽可能多地完成任何特定事情所需的时间是一个非常好的主意。
如果多个任务处于活动状态且执行的工作不够轻松和/或无法共享单个核心,则内核将在线提供额外的核心(从技术上讲,他们不会再睡觉了 - 他们从不离线)如果可用,再次,以确保顺利的性能。现在这缩放roughly about linearly, especially, in mobile ARM processors, according to NVIDIA(PDF)。
当处理器没有任何工作要做时,内核会在可能的情况下将其置于休眠状态,这通常会消耗极少量的电量,从而大大增加了设备在电池上的运行时间。 / p>
到目前为止,我们基本上已经确定我们应该尽可能少地工作,尽可能快地做我们需要做的事情,并且我们应该通过线程最小化我们的开销。关于这些属性的巧妙之处在于,对它们进行优化也可能会提高性能!所以,不用多说,让我们开始看看我们能做些什么:
当我们使用非阻塞调用时,我们通常最终会进行大量的轮询。这意味着我们只是像疯狂的疯子一样燃烧CPU周期,直到发生某些事情。事件循环是人们做这件事的常用方式,也是不该做的一个很好的例子。
相反,请使用阻止调用。通常,对于诸如IO之类的事情,完成请求可能需要相当长的时间。在这个时候,内核可以允许另一个线程或进程使用CPU(从而减少处理器的总体使用)或者可以使处理器休眠。
换句话说,转过这样的话:
while (!event) {
event = getEvent (read);
}
这样的事情:
read ();
有时,您需要处理大量数据。向量操作允许您更快地处理更多数据(通常 - 在极少数情况下,它们可以更慢并且仅存在兼容性)。因此,对代码进行矢量化通常可以使代码更快地完成任务,从而减少处理资源的使用。
今天,许多编译器可以使用适当的标志进行自动向量化。例如,在gcc
上,标志-ftree-vectorize
将启用自动向量化(如果可用),这可以通过在适当的时候处理更多数据来大规模地加速代码,通常在过程中释放寄存器(因此降低套准压力),这也有减少负荷和储存的有益副作用,从而进一步提高性能。
在某些平台上,供应商可能会提供用于处理某些类型数据的库,这些数据可能对此有所帮助。例如,Accelerate framework by Apple包括处理矢量和矩阵数据的函数。
但是,在某些情况下,您可能希望自己进行矢量化,例如当编译器没有看到矢量化的机会或者没有充分利用机会时,您可能希望自己对代码进行矢量化。这通常在汇编中完成,但如果您使用gcc
或clang
,则可以使用内在函数的形式来编写portable vectorized code(尽管对于具有指定矢量大小的所有平台):
typedef float v4f __attribute__ (((vector_size (16)));
// calculates (r = a * b + c) four floats at a time
void vmuladd (const v4f *a, const v4f *b, const v4f *c, int n) {
int x;
for (x = 0; x < n; x++) {
r[x] = a[x] * b[x];
r[x] = r[x] + c[x];
}
}
这在较旧的平台上可能没用,但这可能会严重提高ARM64和其他现代64位平台(x86_64等)的性能。
还记得我说过如何保持更多核心在线是不好的,因为它耗电了吗?良好:
通过多线程进行并行化并不一定意味着使用更多内核。如果你注意我所说的关于使用阻塞函数的所有内容,线程可以让你在其他线程等待IO时完成工作。话虽这么说,你不应该使用那些额外的线程作为&#34; IO工作者&#34;只是等待IO的线程 - 你最终会重新开始轮询。相反,要划分在线程中完成所需的各个原子任务,这样大部分时间它们都可以独立工作。
消耗更多内核比增加时钟频率(线性与指数)更好。如果您的任务需要进行大量处理,那么在几个线程中分解该处理以便它们可以利用可用的核心可能是有用的。如果这样做,请注意确保线程之间只需要最小的同步;我们不想在等待同步时浪费更多的周期。
如果可能的话,尝试将两种方法结合起来 - 当你有很多事情要做时,将任务并行化,并在你有很多事情要做时并行计算。如果您最终使用线程,请在等待工作时尝试阻止它们(pthreads - Android和iOS上都有POSIX semaphores that can help可用的POSIX线程)并尝试让它们长时间运行。
如果您经常需要创建和销毁线程,那么使用thread pool可能是值得的。如何完成此操作会因您手头的任务而异,但一组队列是实现此目的的常用方法。确保你的游泳池&#39;如果你使用一个线程就没有工作时阻塞线程(这可以再次使用上面提到的POSIX信号量完成)。
尝试尽可能少地做。在可能的情况下,将工作卸载到云中的外部服务器,其中功耗不是关键问题(对于大多数人来说 - 一旦您达到规模,这会发生变化)。
在你必须轮询的情况下,通过调用睡眠功能来减少轮询的频率通常会有所帮助 - 转过这样的事情:
while (!event) {
event = getEvent ();
}
这样的事情:
event = getEvent ();
while (!event) {
sleep (25); // in ms
event = getEvent ();
}
此外,如果您没有实时要求(虽然这可能是将其推送到云端的好例子)或者如果您快速获得大量独立数据,批处理可以很好地工作 - 更改类似这样的内容:
while (!exit) {
event = getEventBlocking ();
process (event);
}
更像这样的事情:
while (!exit) {
int x;
event_type *events[16];
for (x = 0; (x < 16) && availableEvents (); x++) {
events[x] = getEventBlocking ();
}
int y;
for (y = 0; y < x; y++) {
process (events[y]);
}
}
这可以通过指令和数据缓存位置提高速度来提高性能。如果可能的话,可以更进一步(当您选择的平台上有这样的功能时):
while (!exit) {
int x;
event_types **events = getEventsAllBlocking (&x);
int y;
for (y = 0; y < x; y++) {
process (events[y]);
}
}
这样可以提高性能,减少等待和执行函数调用的周期。此外,这种加速可以通过大量数据变得非常明显。
这个很简单:在编译器上启动优化设置。查看文档,了解可以启用的相关优化和基准测试,以了解它们是否可以提高性能和/或降低功耗。
在GCC和clang上,您可以使用标记-O2
启用建议的安全优化。请记住,这可能会使调试稍微困难,因此只能在生产版本上使用它。