现充|junyu33

How to manually measure the CPU frequency of a Risc-V processor

Background

Since I previously left my contact information on relevant forums while setting up a jump server environment for a Risc-V development board, someone reached out to me today. They believed that the Ubuntu image was running on the big core (1.6 GHz) of the CanMV-K230, whereas I had previously emphasized it was on the small core (800 MHz), and they wanted me to take a look.

First, lscpu and cat /proc/cpuinfo are definitely not usable. Also, /sys/bus/cpu/devices/cpu0/cpufreq/ does not exist, so the only option was to try writing code manually to measure the CPU frequency.

Solution

A simple solution is to use the Linux system call clock_gettime to get the current time, then run a certain number of instructions in between to reduce measurement error:

#define NUM_INSTRUCTIONS (1000000000)

asm volatile (
    "1:\n"
    "   nop\n"
    "   addi %[counter], %[counter], -1\n"
    "   bnez %[counter], 1b\n" 
    : 
    : [counter] "r"(NUM_INSTRUCTIONS)  
    : "memory"  
);

The code essentially starts from 1e9, executes a nop instruction (which is actually addi x0, x0, 0), decrements the counter, and finally checks if the counter is zero.

Let's briefly calculate the number of instructions: both addi and bnez take one clock cycle. However, considering modern CPUs perform branch prediction and out-of-order execution, bnez will often be skipped directly. Therefore, in ideal conditions (such as running repeatedly multiple times), this loop would take approximately two clock cycles.

Thus, this code, along with the two system calls before and after the loop, will actually take slightly more than 2e9 clock cycles. Of course, this error is negligible for determining whether it's 1.6 GHz or 800 MHz.

Finally, we can simply divide 2e9 by the elapsed time to get the result in GHz.

The final code is as follows:

#include <stdio.h>
#include <time.h>

#define NUM_INSTRUCTIONS (1 * 1000000000)

int main() {
    struct timespec start, end;
    clock_gettime(CLOCK_REALTIME, &start);

    asm volatile (
        "1:\n"
                                "               nop\n"
        "   addi %[counter], %[counter], -1\n"
        "   bnez %[counter], 1b\n"
        :
        : [counter] "r"(NUM_INSTRUCTIONS)
        : "memory"
    );

    clock_gettime(CLOCK_REALTIME, &end);

    long long start_time_ns = start.tv_sec * 1000000000 + start.tv_nsec;
    long long end_time_ns = end.tv_sec * 1000000000 + end.tv_nsec;
    long long elapsed_time_ns = end_time_ns - start_time_ns;

    printf("Elapsed time: %lld ns\n", elapsed_time_ns);
                printf("Elapsed Freq: %.3f GHz\n", 2e9 / elapsed_time_ns);

    return 0;
}

Execution results:

junyu33@zjy-canmv:~/tmp$ ./test
Elapsed time: 1296113478 ns
Elapsed Freq: 1.543 GHz
junyu33@zjy-canmv:~/tmp$ ./test
Elapsed time: 1297572988 ns
Elapsed Freq: 1.541 GHz
junyu33@zjy-canmv:~/tmp$ ./test
Elapsed time: 1294495232 ns
Elapsed Freq: 1.545 GHz
junyu33@zjy-canmv:~/tmp$ ./test
Elapsed time: 1294133798 ns
Elapsed Freq: 1.545 GHz
junyu33@zjy-canmv:~/tmp$ ./test
Elapsed time: 1294905847 ns
Elapsed Freq: 1.545 GHz

Summary

That friend eventually used my code to test his development board and got a result of 1.598 GHz. So, he was correct.