所以最近(今天)我学会了cpu分支预测的概念。 基本上,如果if语句的比较是可预测的,那么您的代码将运行得更快。 下面是我写的一个程序(C#中的控制台应用程序),它演示了这个概念:
using System;
using System.Collections.Generic;
using System.Diagnostics;
/* *
* Below are the times for branch prediction / misfires in seconds:
*
* Predictable: 0.91
* Unpredictable: 1.61
*
* Summary: When the branch is predictable, the program runs 55% faster.
*/
namespace BranchPredictions
{
class Program
{
static void Main(string[] args)
{
const int MAX = 100000000; // The amount of branches to create
bool predictable = true; // When true the list isn't in a predictable order
var nums = new List<int>(MAX);
var random = new Random();
for (int i = 0; i < MAX; i++)
{
if (predictable)
{
nums.Add(i);
}
else
{
nums.Add(random.Next());
}
}
int count = 0;
var sw = Stopwatch.StartNew();
foreach (var num in nums)
{
if (num % 2 == 0) // Here is the branch
{
count++;
}
}
sw.Stop();
Console.WriteLine("Total count: {0}", count);
Console.WriteLine("Time taken: {0}", sw.Elapsed);
if (Debugger.IsAttached)
{
Console.Write("Press any key to continue..");
Console.ReadKey(true);
}
}
}
}
通过了解一些硬件概念,它让我眼前一亮,我可以让某些代码运行得更快,而不需要任何真正的代码更改!
但它让我想知道,如果我知道它是什么硬件还能使我的软件运行得更快呢?
我使用的是Windows和C#,但这些概念适用于所有计算机和语言。
答案 0 :(得分:1)
首先作为对分支预测的讨论的旁注 - 可以提示编译器关于可预测性问题以产生使用例如的恒定速度代码。 CMOV。人们通常也可以将if-else-constructions转换为常量时间表达式,例如count += 1 - (num % 2);
来验证任何分支预测假设。
要利用/考虑的其他主要硬件概念是内存带宽和缓存。拆分例如大的计算10000x10000阵列到1250x1250 x 8x8块利用缓存局部性的概念。
当“char”足够时,内存带宽可以通过“微管理”数组元素的大小而不必使用“int”来考虑。
高速缓存的N路相关性可能会导致某些数组长度比其他数据库更快,因为某些内存地址在同一个高速缓存行上竞争;治愈是过度分配。
循环展开通常更多地创建独立的变量依赖流,而不是分支预测。在没有直接进行并行编程的情况下,在某些情况下,当某些指令等待前一个结果时,通过为处理器找到一些有用的计算,在同一个循环中混合两个独立的处理任务可以使速度加倍或加倍。指令吞吐量与延迟解释了这种现象。
答案 1 :(得分:1)
查看缓存及其(有时是看似疯狂的)效果。这是一篇很好的论文,可以帮助您入门:What Every Programmer Should Know About Memory
很多时候,缓存“只是在那里”,悄悄地使你的程序运行得比它不得不一直打到dram要快得多。但是它能如何完成它的工作取决于你如何访问内存。一些常见的编程结构可以帮助它(比如以简单的方式迭代数组),其他常见的编程结构实际上很糟糕(比如遍历链表)。
缓存世界往往与传统智慧相悖。例如,经常适用的旧的“时间/内存”权衡也经常被完全颠倒 - 更少的内存通常意味着更少的缓存未命中,并且这可能比执行更多的数学(相比之下具有微不足道的成本)更重要 - 您可以轻松地执行几百条简单指令,了解最后一级缓存未命中所需的时间。此外,几乎总是这样的情况是,数组列表比链表更快,即使你做了很多插入和删除,这是链表应该擅长的。链接列表与缓存不兼容 - 它们在指针上浪费了很多(每个项目1或2个),并且它们经常以不可预测的模式访问内存。
最初反直觉的还有一个简单的效果,即如果你访问某个内存,很快就会再次访问它(或在它附近,在同一个缓存行中)。代码通常通过删除冗余读取来“优化”,但它们并不是真正的问题。真正的问题是读取错过了缓存。缓存命中不是完全免费的,如果你做的足够多,你会看到它们的影响。但是不要专注于它们,专注于未命中。
但是缓存的世界更加怪异。相关性效应可以使得访问具有特定步幅的阵列突然比稍微不同的步幅慢得多。这通常出现在迭代列的矩阵中(如果它是行主矩阵),这转换为访问具有等于矩阵宽度的步幅的1D阵列。某些宽度(通常是两个幂的倍数)突然使该过程意外地变慢。
好像这还不够,请注意,由于缓存的大小有限,因此将其用于一件事意味着其他东西会消失。这会导致非局部影响。特别是,如果你调用一个子程序,现在可能不会有一些以前在缓存中的东西。这可能会导致如果以更多缓存使用为代价使子程序更快,则可能会使调用者变慢 - 可能与您在子例程中保存的数量相同(或者更多)。
所以这里有几个简单的事情可以做:
这些提示都不是真正的魔术。始终以一切为基准。