测量处理器频率的常用算法有哪些?
10 回答
Core Duo 之后的 Intel CPU 支持两个特定于型号的寄存器,称为 IA32_MPERF 和 IA32_APERF。
MPERF 以 CPU 支持的最大频率计数,而 APERF 以实际当前频率计数。
实际频率由下式给出:
您可以使用此流程阅读它们
; read MPERF
mov ecx, 0xe7
rdmsr
mov mperf_var_lo, eax
mov mperf_var_hi, edx
; read APERF
mov ecx, 0xe8
rdmsr
mov aperf_var_lo, eax
mov aperf_var_hi, edx
但请注意,rdmsr 是一条特权指令,只能在 ring 0 中运行。
我不知道操作系统是否提供了读取这些的接口,尽管它们的主要用途是用于电源管理,因此它可能不提供这样的接口。
我将在这个答案中用各种细节与自己约会,但到底是什么......
几年前我不得不在基于 Windows 的 PC 上解决这个问题,所以我处理的是 Intel x86 系列处理器,如 486、Pentium 等。这种情况下的标准算法是执行一长串 DIVide 指令,因为这些指令通常是 Intel 集中受 CPU 限制最多的单条指令。因此内存预取和其他架构问题不会对指令执行时间产生实质性影响——预取队列总是满的,并且指令本身不会触及任何其他内存。
您可以使用在您运行的环境中可以访问的最高分辨率时钟对其进行计时。(在我的情况下,我在兼容 PC 的启动时间附近运行,所以我直接在主板上对定时器芯片进行编程。不是推荐在真正的操作系统中,通常这些天有一些适当的 API 可以调用)。
您必须处理的主要问题是不同的 CPU 类型。当时有英特尔、AMD 和一些较小的供应商(如 Cyrix)制造 x86 处理器。相对于 DIV 指令,每个模型都有自己的性能特征。我的汇编计时函数只会返回一定数量的 DIV 指令在紧密循环中完成的时钟周期数。
因此,我所做的是从运行我想要计时的每个处理器型号的实际 PC 中收集一些计时(来自该函数的原始返回值),并根据已知的处理器速度和处理器类型将这些计时记录在电子表格中。实际上,我有一个命令行工具,它只是我的计时功能的一个薄壳,我会将磁盘放入计算机商店并从显示模型中获取计时!(当时我在一家很小的公司工作)。
使用这些原始时序,我可以绘制一个理论图表,说明在该特定 CPU 的任何已知速度下我应该获得什么时序。
诀窍是:我总是讨厌你运行一个实用程序,它会宣布你的 CPU 是 99.8 Mhz 或其他什么。显然它是 100 Mhz,测量中只有一个小的舍入误差。在我的电子表格中,我记录了每个处理器供应商销售的实际速度。然后我会使用实际时间图来估计任何已知速度的预计时间。但我会沿着线建立一个点表,在这些点上时间应该四舍五入到下一个速度。
换句话说,如果 100 个滴答声进行所有重复划分意味着 500 Mhz,而 200 个滴答声意味着 250 Mhz,那么我将建立一个表格,说明低于 150 的任何东西都是 500 Mhz,高于 150 的任何东西都是 250 Mhz。(假设这是该芯片供应商提供的仅有的两种速度)。这很好,因为即使 PC 上的一些奇怪的软件会影响我的时间,最终结果通常仍然是死机。
当然,现在,在超频、电源管理的动态时钟速度和其他此类技巧的时代,这样的方案将不太实用。至少在运行计时功能之前,您需要先确保 CPU 处于其最高动态选择的速度。
好的,我现在回去把孩子们从我的草坪上赶走。
自 Pentium 以来,x86 Intel CPU 上的一种方法是使用 RDTSC 指令的两个采样,并使用已知壁时间的延迟循环,例如:
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
uint64_t rdtsc(void) {
uint64_t result;
__asm__ __volatile__ ("rdtsc" : "=A" (result));
return result;
}
int main(void) {
uint64_t ts0, ts1;
ts0 = rdtsc();
sleep(1);
ts1 = rdtsc();
printf("clock frequency = %llu\n", ts1 - ts0);
return 0;
}
(在带有 GCC 的 32 位平台上)
如果设置了 CR4 中的 TSC 标志,RDTSC 在 ring 3 中可用,这很常见但不能保证。这种方法的一个缺点是它很容易受到频率缩放变化的影响,如果它们发生在延迟之内,则会影响结果。为了缓解这种情况,您可以执行使 CPU 保持忙碌的代码并不断轮询系统时间以查看您的延迟期是否已过期,以使 CPU 保持在可用的最高频率状态。
我使用以下(伪)算法:
basetime=time(); /* time returns seconds */
while (time()==basetime);
stclk=rdtsc(); /* rdtsc is an assembly instruction */
basetime=time();
while (time()==basetime
endclk=rdtsc();
nclks=encdclk-stclk;
此时,您可能会假设您已经确定了时钟频率,但即使它看起来正确,也可以改进。
所有 PC 都包含一个 PIT(可编程间隔定时器)设备,其中包含(曾经)用于串行端口和系统时钟的计数器。它的馈电频率为 1193182 Hz。系统时钟计数器设置为最高倒计时值 (65536),导致系统时钟滴答频率为 1193182/65536 => 18.2065 Hz 或每 54.925 毫秒一次。
因此,时钟递增到下一秒所需的滴答数将取决于。通常需要 18 个刻度,有时需要 19 个刻度。这可以通过执行算法(如上)两次并存储结果来处理。这两个结果将等于两个 18 滴答序列或一个 18 和一个 19。不会出现连续两个 19。因此,通过取两个结果中较小的一个,您将获得 18 个滴答秒。通过乘以 18.2065 并除以 18.0 或使用整数运算,乘以 182065,加 90000 并除以 180000 来调整此结果。90000 是 180000 的一半,用于舍入。如果您选择使用整数路由进行计算,请确保您使用的是 64 位乘法和除法。
您现在将拥有一个以 Hz 为单位的 CPU 时钟速度 x,它可以转换为 kHz ((x+500)/1000) 或 MHz ((x+5000000)/1000000)。500 和 500000 分别是 1000 和 1000000 的二分之一,用于四舍五入。计算 MHz 不要通过 kHz 值,因为可能会出现舍入问题。使用 Hz 值和第二种算法。
这就是BogoMIPS之类的东西的意图,但现在 CPU 复杂得多。超标量 CPU 可以在每个时钟发出多条指令,从而使基于时钟周期计数的任何测量都非常不准确。
CPU 频率也会根据提供的负载和/或温度而变化。CPU 当前以 800 MHz 运行的事实并不意味着它将始终以 800 MHz 运行,它可能会根据需要调高或调低。
如果确实需要知道时钟频率,应该作为参数传入。板上的 EEPROM 将提供基本频率,如果时钟可以变化,您需要能够读取 CPU 电源状态寄存器(或进行操作系统调用)以找出当时的频率。
尽管如此,可能还有其他方法可以完成您想要做的事情。例如,如果您想对特定代码路径花费多长时间进行高精度测量,CPU 可能具有以固定频率运行的性能计数器,这比读取滴答计数寄存器更好地测量挂钟时间。
“lmbench”为不同架构提供可移植的cpu频率算法。
它运行一些不同的循环,处理器的时钟速度是各种循环执行频率的最大公约数。
当我们能够获得具有相对质数的循环计数时,此方法应该始终有效。
在 Intel CPU 上,获取当前(平均)CPU 频率的常用方法是从几个 CPU 计数器计算它:
CPU_freq = tsc_freq * (aperf_t1 - aperf_t0) / (mperf_t1 - mperf_t0)
TSC(时间戳计数器)可以使用专用的 x86 指令从用户空间读取,但其频率必须通过时钟校准来确定。最好的方法是从内核(已经完成校准)获取 TSC 频率。
aperf 和 mperf 计数器是特定于模型的寄存器MSR,需要 root 权限才能访问。同样,有专门的 x86 指令用于访问 MSR。
由于 perf 计数器速率与 TSC 速率成正比,而 aperf 速率与 CPU 频率成正比,您可以通过上述等式得到 CPU 频率。
当然,如果 CPU 频率在您的t0 - t1
时间增量中发生变化(例如由于频率缩放),您可以使用此方法获得平均 CPU 频率。
我写了一个小工具cpufreq可以用来测试这个方法。
也可以看看:
一种选择是通过在每个循环中运行具有已知指令的代码来检测 CPU 频率
这个功能包含在 7zip 中,因为我认为大约是 v9.20。
> 7z b
7-Zip 9.38 beta Copyright (c) 1999-2014 Igor Pavlov 2015-01-03
CPU Freq: 4266 4000 4266 4000 2723 4129 3261 3644 3362
最终数字应该是正确的(在我的 PC 和许多其他设备上,我发现它非常正确 - 测试运行得非常快,因此 turbo 可能无法启动,并且设置为平衡/省电模式的服务器很可能会给出1GHz左右的读数)
源代码在GitHub(官方源是从 7-zip.org 下载)
最重要的部分是:
#define YY1 sum += val; sum ^= val;
#define YY3 YY1 YY1 YY1 YY1
#define YY5 YY3 YY3 YY3 YY3
#define YY7 YY5 YY5 YY5 YY5
static const UInt32 kNumFreqCommands = 128;
EXTERN_C_BEGIN
static UInt32 CountCpuFreq(UInt32 sum, UInt32 num, UInt32 val)
{
for (UInt32 i = 0; i < num; i++)
{
YY7
}
return sum;
}
EXTERN_C_END
我不确定你为什么需要为此组装。如果您在具有 /proc 文件系统的机器上,则运行:
> cat /proc/cpuinfo
可能会给你你需要的东西。