如何使用C编程语言找到CPU频率

19

我正在尝试找出一种方法,以获取运行我的C代码的系统的CPU频率。

为了澄清,我正在寻找一个抽象的解决方案(不会绑定到特定的架构或操作系统),它可以让我了解计算机的操作频率,而我的代码正在执行。我不需要非常精确,但我想要在球场范围内(例如,我有一个2.2GHz处理器,我想能够在我的程序中得知我在几百MHz左右)

有人有使用标准C代码的想法吗?


3
不要重复造轮子。操作系统管理硬件并已具备此功能,因此需要找到一种方法来检测程序正在执行的操作系统,然后相应地提取CPU频率。 - Alex W
4
这基本上是没有意义的。假设你有一个程序,在现代多任务操作系统下运行,安装在虚拟云服务器上。时钟速度的含义是什么?即使在微控制器上裸机运行且中断已禁用,使用零等待状态的内部存储器,如果不知道程序编译后的指令及每个指令需要多少个时钟周期,那么“时钟速度”有何相关性呢? - Chris Stratton
1
这个主题可能会激发您的灵感:https://dev59.com/303Sa4cB1Zd3GeqPxrox谢谢。 - TOC
1
你做不到。标准C(由某个英文规范文件定义)甚至不应该在硬件上运行 - 你可以在模拟器中运行,或者不道德地使用一组人类奴隶来解释你的代码。因此,在标准C中,CPU及其频率的概念是毫无意义的。当然,对于某些特定的操作系统和API,有一些具体的答案。(在Linux上,按顺序阅读/proc/cpuinfo - Basile Starynkevitch
11
我认为许多 Stack Overflow 上的人都无法通过图灵测试。任何稍微模糊不清的东西都会返回语法错误。我找到了一种使用 C/C++ 内嵌函数来查找真实 Intel 处理器(而不是虚拟处理器)操作频率的解决方案,而人们则争论什么是标准 C。我会理解程序员吗?还有人关心硬件吗? - Z boson
显示剩余4条评论
5个回答

18

为了完整起见,已经有一个简单、快速、准确的用户模式解决方案,但它有一个巨大的缺点:它仅适用于英特尔Skylake、Kabylake和更新的处理器。确切的要求是CPUID级别16h支持。根据英特尔软件开发人员手册325462发布59页770的说法:

  • CPUID.16h.EAX = 处理器基础频率(以MHz为单位);

  • CPUID.16h.EBX = 最大频率(以MHz为单位);

  • CPUID.16h.ECX = 总线(参考)频率(以MHz为单位)。

Visual Studio 2015示例代码:

#include <stdio.h>
#include <intrin.h>

int main(void) {
    int cpuInfo[4] = { 0, 0, 0, 0 };
    __cpuid(cpuInfo, 0);
    if (cpuInfo[0] >= 0x16) {
        __cpuid(cpuInfo, 0x16);

        //Example 1
        //Intel Core i7-6700K Skylake-H/S Family 6 model 94 (506E3)
        //cpuInfo[0] = 0x00000FA0; //= 4000 MHz
        //cpuInfo[1] = 0x00001068; //= 4200 MHz
        //cpuInfo[2] = 0x00000064; //=  100 MHz

        //Example 2
        //Intel Core m3-6Y30 Skylake-U/Y Family 6 model 78 (406E3)
        //cpuInfo[0] = 0x000005DC; //= 1500 MHz
        //cpuInfo[1] = 0x00000898; //= 2200 MHz
        //cpuInfo[2] = 0x00000064; //=  100 MHz

        //Example 3
        //Intel Core i5-7200 Kabylake-U/Y Family 6 model 142 (806E9)
        //cpuInfo[0] = 0x00000A8C; //= 2700 MHz
        //cpuInfo[1] = 0x00000C1C; //= 3100 MHz
        //cpuInfo[2] = 0x00000064; //=  100 MHz

        printf("EAX: 0x%08x EBX: 0x%08x ECX: %08x\r\n", cpuInfo[0], cpuInfo[1], cpuInfo[2]);
        printf("Processor Base Frequency:  %04d MHz\r\n", cpuInfo[0]);
        printf("Maximum Frequency:         %04d MHz\r\n", cpuInfo[1]);
        printf("Bus (Reference) Frequency: %04d MHz\r\n", cpuInfo[2]);
    } else {
        printf("CPUID level 16h unsupported\r\n");
    }
    return 0;
}

1
在CPUID中是否报告当前频率? - osgx
3
这只报告了“盒子上标注”的频率,这可能与真实频率略有不同,因为“参考频率”只是一个名义值(例如,实际BCLK与100 MHz的参考值可以相差很大),并且由于自动频率缩放(涡轮增压、速度调节等)或手动频率限制(例如,由于节能而由操作系统强制施加),以及其他几个原因,它可能会有非常大的差异。 - BeeOnRope

16

可以找到一种通用解决方案,以正确地获得单个线程或多个线程的操作频率。这不需要管理员/根权限或访问模型特定寄存器。我已经在Linux和Windows上测试过它,在Intel处理器上包括Nahalem、Ivy Bridge和Haswell,使用一个插槽到四个插槽(40个线程)。所有结果与正确答案的偏差均小于0.5%。在向您展示如何执行此操作之前,请先查看结果(来自GCC 4.9和MSVC2013):

Linux:    E5-1620 (Ivy Bridge) @ 3.60GHz    
1 thread: 3.789, 4 threads: 3.689 GHz:  (3.8-3.789)/3.8 = 0.3%, 3.7-3.689)/3.7 = 0.3%

Windows:  E5-1620 (Ivy Bridge) @ 3.60GHz
1 thread: 3.792, 4 threads: 3.692 GHz: (3.8-3.789)/3.8 = 0.2%, (3.7-3.689)/3.7 = 0.2%

Linux:  4xE7-4850 (Nahalem) @ 2.00GHz
1 thread: 2.390, 40 threads: 2.125 GHz:, (2.4-2.390)/2.4 = 0.4%, (2.133-2.125)/2.133 = 0.4%

Linux:    i5-4250U (Haswell) CPU @ 1.30GHz
1 thread: within 0.5% of 2.6 GHz, 2 threads wthin 0.5% of 2.3 GHz

Windows: 2xE5-2667 v2 (Ivy Bridge) @ 3.3 GHz
1 thread: 4.000 GHz, 16 threads: 3.601 GHz: (4.0-4.0)/4.0 = 0.0%, (3.6-3.601)/3.6 = 0.0%

我从这个链接http://randomascii.wordpress.com/2013/08/06/defective-heat-sinks-causing-garbage-gaming/得到了这个想法。

如果要做这件事,首先要做的是20年前所做的。您需要编写一些带有循环的代码,并计算出延迟时间。以下是我使用的代码:

static int inline SpinALot(int spinCount)
{
    __m128 x = _mm_setzero_ps();
    for(int i=0; i<spinCount; i++) {
        x = _mm_add_ps(x,_mm_set1_ps(1.0f));
    }
    return _mm_cvt_ss2si(x);
}

这段代码存在载入循环依赖,因此CPU无法重新排序以减少延迟。每次迭代始终需要3个时钟周期。操作系统不会将线程迁移到另一个核心,因为我们会绑定线程。

然后在每个物理核上运行此函数。我使用OpenMP实现了这一点。必须为这些线程绑定处理器核心。在Linux中,您可以使用export OMP_PROC_BIND=true来绑定线程,并假设您有ncores个物理核心,则还需使用export OMP_NUM_THREADS=ncores。如果您想编程地绑定和查找Intel处理器的物理核心数,请参考programatically-detect-number-of-physical-processors-cores-or-if-hyper-threadingthread-affinity-with-windows-msvc-and-openmp

void sample_frequency(const int nsamples, const int n, float *max, int nthreads) {
    *max = 0;
    volatile int x = 0;
    double min_time = DBL_MAX;
    #pragma omp parallel reduction(+:x) num_threads(nthreads)
    {
        double dtime, min_time_private = DBL_MAX;
        for(int i=0; i<nsamples; i++) {
             #pragma omp barrier
             dtime = omp_get_wtime();
             x += SpinALot(n);
             dtime = omp_get_wtime() - dtime;
             if(dtime<min_time_private) min_time_private = dtime;
        }
        #pragma omp critical
        {
            if(min_time_private<min_time) min_time = min_time_private;
        }
    }
    *max = 3.0f*n/min_time*1E-9f;
}

最后在循环中运行采样器并打印结果

int main(void) {
    int ncores = getNumCores();
    printf("num_threads %d, num_cores %d\n", omp_get_max_threads(), ncores);       
    while(1) {
        float max1, median1, max2, median2;
        sample_frequency(1000, 1000000, &max2, &median2, ncores);
        sample_frequency(1000, 1000000, &max1, &median1,1);          
        printf("1 thread: %.3f, %d threads: %.3f GHz\n" ,max1, ncores, max2);
    }
}

我没有在AMD处理器上进行过测试。我认为带有模块(例如Bulldozer)的AMD处理器将不得不绑定到每个模块而不是每个AMD“核心”。这可以通过GCC中的export GOMP_CPU_AFFINITY来完成。你可以在https://bitbucket.org/zboson/frequency找到一个完整的工作示例,该示例适用于Windows和Linux上的Intel处理器,并将正确地找到Intel处理器的物理核心数量(至少从Nahalem开始),并将它们绑定到每个物理核心(不使用MSVC不支持的OMP_PROC_BIND)。


由于SSE、AVX和AVX512的不同频率缩放,必须对此方法进行一些修改以适应现代处理器。

以下是我使用四个Xeon 6142处理器(每个处理器16个核心)修改我的方法后得到的新表格(请参见表格后面的代码)。

        sums  1-thread  64-threads
SSE        1       3.7         3.3
SSE        8       3.7         3.3
AVX        1       3.7         3.3
AVX        2       3.7         3.3
AVX        4       3.6         2.9
AVX        8       3.6         2.9
AVX512     1       3.6         2.9
AVX512     2       3.6         2.9
AVX512     4       3.5         2.2
AVX512     8       3.5         2.2

这些数字与此表中的频率相符 https://en.wikichip.org/wiki/intel/xeon_gold/6142#Frequencies

有趣的是,我现在需要进行至少4个并行求和才能实现较低的频率。Skylake上addps的延迟为4个时钟周期。这些可以通过两个端口 (使用AVX512端口0和1进行计数,一个AVX512端口和其他AVX512操作进入端口5)。

以下是我是如何进行八个并行求和的。

static int inline SpinALot(int spinCount) {
  __m512 x1 = _mm512_set1_ps(1.0);
  __m512 x2 = _mm512_set1_ps(2.0);
  __m512 x3 = _mm512_set1_ps(3.0);
  __m512 x4 = _mm512_set1_ps(4.0);
  __m512 x5 = _mm512_set1_ps(5.0);
  __m512 x6 = _mm512_set1_ps(6.0);
  __m512 x7 = _mm512_set1_ps(7.0);
  __m512 x8 = _mm512_set1_ps(8.0);
  __m512 one = _mm512_set1_ps(1.0);
  for(int i=0; i<spinCount; i++) {
    x1 = _mm512_add_ps(x1,one);
    x2 = _mm512_add_ps(x2,one);
    x3 = _mm512_add_ps(x3,one);
    x4 = _mm512_add_ps(x4,one);
    x5 = _mm512_add_ps(x5,one);
    x6 = _mm512_add_ps(x6,one);
    x7 = _mm512_add_ps(x7,one);
    x8 = _mm512_add_ps(x8,one);
  }
  __m512 t1 = _mm512_add_ps(x1,x2);
  __m512 t2 = _mm512_add_ps(x3,x4);
  __m512 t3 = _mm512_add_ps(x5,x6);
  __m512 t4 = _mm512_add_ps(x7,x8);
  __m512 t6 = _mm512_add_ps(t1,t2);
  __m512 t7 = _mm512_add_ps(t3,t4);
  __m512  x = _mm512_add_ps(t6,t7);
  return _mm_cvt_ss2si(_mm512_castps512_ps128(x));
}

2
这在Skylake中已经出现了问题,它改变了addps的延迟。 - harold
2
你最好使用延迟为1个周期的简单指令,因为这不太可能在未来变得更糟(或更好!)。例如,一系列依赖加法。一个问题是,你必须确保要么禁用矢量化,要么使你的循环不适合矢量化才能保证可靠性... - BeeOnRope
1
这并不是说你不能在这些情况下获得半合理的结果;特别是,一旦你将某个东西运行到100%的CPU上几百毫秒,次谐波频率就大多被消除了。然而,Turbo永远无法解决(除非你改变msr regs的乘数),而在我的情况下,禁用Turbo可以将测量的稳定性提高两个数量级。我可以将基于时间的测量降至约0.1%或0.01%,但使用Turbo时误差通常超过1%。@Zboson - BeeOnRope
1
标量加法在许多Netburst CPU上不起作用,因为双泵ALU会使表面频率加倍(但似乎所有64位型号都可能已经放弃了双泵操作,因此它可能适用于所有64位型号),但这绝对是一个好方法。斐波那契数列是一个有趣的想法,可以防止编译器优化循环。实现b = a + 2*b只需一个add和一个lea,可以将时间减半,但我检查过的编译器都没有实现它。 - BeeOnRope
2
@PeterCordes - 这是我的意思,用于快速斐波那契数列。fib2计算斐波那契数列(通过printf验证),但依赖链长度减半。请注意,gcc“撤销”了我的优秀工作,并使用普通的add,但clanggcc使用lea。请注意,我将循环展开了另外两次以表达计算的两个阶段,但您也可以使用临时变量a来完成。我将其添加为fib3,更接近您编写的版本。然而,编译器对此并不太擅长。 - BeeOnRope
显示剩余12条评论

8
你如何找到CPU频率取决于架构和操作系统,并且没有抽象的解决方案。
如果我们回到20多年前,你正在使用一个没有上下文切换并且CPU按顺序执行给定指令的操作系统,你可以在循环中编写一些C代码并计时,然后基于它被编译成的汇编代码在运行时计算指令数。这已经假设每个指令都需要1个时钟周期,但自从流水线处理器出现以来,这是一个相当糟糕的假设。
但是任何现代操作系统都会在多个进程之间进行切换。即使如此,您也可以尝试计时一堆相同的for循环运行(忽略页面故障和其他许多原因导致处理器可能停滞的时间),并获得中位数值。
即使先前的解决方案有效,您也有多发处理器。对于任何现代处理器,重新排序指令、在同一时钟周期内发出一堆指令,甚至将它们分割成不同的核心都是公平竞争的。

是的,这基本上就是我想到的。我只是抱着希望,希望我错过了一些愚蠢的东西。有没有什么方法可以防止任务切换,强制CPU运行单个上下文,并进行测量...或者类似的东西。可能要求太多了。感谢您的建议。 - Mike

2
CPU频率是硬件相关的事情,所以没有通用的方法可以获取它,它也取决于你使用的操作系统。

例如,如果你正在使用Linux,你可以读取文件/proc/cpuinfo或解析dmesg启动日志来获取这个值,或者你可以在这里查看Linux内核如何处理这些内容,并尝试自定义代码以满足你的需求:

https://github.com/torvalds/linux/blob/master/arch/x86/kernel/cpu/proc.c

谢谢。


0

我猜从软件中获取时钟频率的一种方法是将硬件参考手册(HRM)的知识硬编码到软件中。您可以从软件中读取时钟配置寄存器。假设您知道源时钟频率,软件可以使用时钟寄存器中的乘法器和除数值,并根据HRM中提到的适当公式来推导时钟频率。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接