17

我试图找出是否有办法了解我的 C 代码正在运行的系统的 CPU 频率。

澄清一下,我正在寻找一个抽象的解决方案(一个不会绑定到特定架构或操作系统的解决方案),它可以让我了解我的代码正在执行的计算机的运行频率。我不需要准确,但我想在球场上(即我有一个 2.2GHz 处理器,我希望能够在我的程序中告诉我我在几百兆赫)

有人知道使用标准 C 代码吗?

4

5 回答 5

16

为了完整起见,已经有一个简单、快速、准确的用户模式解决方案存在一个巨大的缺点:它仅适用于 Intel 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;
}
于 2016-09-21T14:57:40.867 回答
15

可以找到一个通用的解决方案,为一个线程或多个线程正确获取工作频率。这不需要管理员/根权限或访问模型特定的寄存器。我已经在英特尔处理器上的 Linux 和 Windows 上对此进行了测试,包括 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 做到了这一点。线程必须为此绑定。在带有 GCC 的 linux 中,您可以使用export OMP_PROC_BIND=true绑定线程并假设您也有ncores物理内核export OMP_NUM_THREADS=ncores。如果您想以编程方式绑定并查找英特尔处理器的物理内核数量,请参阅此programatically-detect-number-of-physical-processors-cores-or-if-hyper-threadingthread-affinity-with-windows-msvc-和-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 处理器上测试过这个。我认为带有模块的 AMD 处理器(例如 Bulldozer)必须绑定到每个模块而不是每个 AMD“核心”。这可以通过export GOMP_CPU_AFFINITYGCC 来完成。您可以在https://bitbucket.org/zboson/frequency找到一个完整的工作示例,该示例适用于英特尔处理器上的 Windows 和 Linux,并且将正确找到英特尔处理器的物理内核数量(至少从 Nahalem 开始)并将它们绑定到每个物理核心(不使用OMP_PROC_BINDMSVC 没有的)。


由于 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));
}
于 2014-08-20T08:36:57.863 回答
7

您如何找到 CPU 频率取决于架构和操作系统,并且没有抽象的解决方案。

如果我们是 20 多年前,并且您使用的操作系统没有上下文切换,并且 CPU 按顺序执行给定的指令,您可以在循环中编写一些 C 代码并对其计时,然后根据它编译成的程序集计算运行时的指令数。这已经假设每条指令需要 1 个时钟周期,这是自流水线处理器以来的一个相当糟糕的假设。

但是任何现代操作系统都会在多个进程之间切换。即使这样,您也可以尝试为一堆相同的for循环运行计时(忽略页面错误所需的时间以及处理器可能会停止的多种其他原因)并获得一个中值。

即使以前的解决方案有效,您也有多个问题处理器。对于任何现代处理器,重新排序指令、在同一时钟周期内发出一堆指令,甚至将它们拆分到内核之间都是公平的游戏。

于 2012-07-29T04:30:08.490 回答
2

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

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

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

问候。

于 2012-07-29T04:59:30.440 回答
0

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

于 2017-03-06T17:19:49.973 回答