【问题标题】:Quick way to count number of instructions executed in a C program快速计算 C 程序中执行的指令数的方法
【发布时间】:2012-10-30 02:15:09
【问题描述】:

有没有一种简单的方法可以在执行 C 程序时快速计算执行的指令数(x86 指令 - 每条指令和多少条指令)?

我在x86_64 GNU/Linux 机器上使用gcc version 4.7.1 (GCC)。

【问题讨论】:

  • 我同意 Doness 的回答,即人们通常希望分析每个函数的执行时间。然而,如果你真的想得到每条指令执行的准确计数,那么你需要在指令集模拟器上运行你的代码,例如simplescalar.com
  • 您能详细说明您要完成的工作吗?在 x86 上,指令执行性能取决于上下文,而不是实际指令——例如,几乎所有指令都可以选择性地加载或存储。纯粹的寄存器到寄存器指令将以复杂的方式依赖于现代 CPU 上的流水线状态。这听起来对我来说不是有用的信息。
  • 你为什么要问?通常 profiling 意味着不同的东西...例如使用gcc -pg -Wall -O 编译并使用gprof 或者oprofile !!
  • 我正在实现一个复杂的数学算法,我想计算在其执行过程中发生的乘法(和除法)的数量。我正在寻找一种简单的方法,而不是查看高级代码和推断数字。也许我应该使用自定义乘法函数并在其中插入一个计数器。
  • 我不确定我是否相信“零等待内存”,即使现代 CPU 上的 L1 缓存也是 4 个周期!但无论如何:看起来像使用自定义 operator*() 实现在 C++ 中构建应用程序这样的技巧。请注意,在现代编译器上,即使是“乘法”也可能无法以易于检测的方式实现(考虑使用 LEA 指令的经典技巧)。

标签: c linux profile


【解决方案1】:

Linux perf_event_open 系统调用config = PERF_COUNT_HW_INSTRUCTIONS

此 Linux 系统调用似乎是性能事件的跨体系结构包装器,包括来自 CPU 的硬件性能计数器和来自内核的软件事件。

这是一个改编自 man perf_event_open 页面的示例:

perf_event_open.c

#define _GNU_SOURCE
#include <asm/unistd.h>
#include <linux/perf_event.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <inttypes.h>
#include <sys/types.h>

static long
perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
                int cpu, int group_fd, unsigned long flags)
{
    int ret;

    ret = syscall(__NR_perf_event_open, hw_event, pid, cpu,
                    group_fd, flags);
    return ret;
}

int
main(int argc, char **argv)
{
    struct perf_event_attr pe;
    long long count;
    int fd;

    uint64_t n;
    if (argc > 1) {
        n = strtoll(argv[1], NULL, 0);
    } else {
        n = 10000;
    }

    memset(&pe, 0, sizeof(struct perf_event_attr));
    pe.type = PERF_TYPE_HARDWARE;
    pe.size = sizeof(struct perf_event_attr);
    pe.config = PERF_COUNT_HW_INSTRUCTIONS;
    pe.disabled = 1;
    pe.exclude_kernel = 1;
    // Don't count hypervisor events.
    pe.exclude_hv = 1;

    fd = perf_event_open(&pe, 0, -1, -1, 0);
    if (fd == -1) {
        fprintf(stderr, "Error opening leader %llx\n", pe.config);
        exit(EXIT_FAILURE);
    }

    ioctl(fd, PERF_EVENT_IOC_RESET, 0);
    ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);

    /* Loop n times, should be good enough for -O0. */
    __asm__ (
        "1:;\n"
        "sub $1, %[n];\n"
        "jne 1b;\n"
        : [n] "+r" (n)
        :
        :
    );

    ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
    read(fd, &count, sizeof(long long));

    printf("Used %lld instructions\n", count);

    close(fd);
}

编译运行:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o perf_event_open.out perf_event_open.c
./perf_event_open.out

输出:

Used 20016 instructions

所以我们看到结果非常接近预期值 20000:10k * __asm__ 块中每个循环两条指令(sub、jne)。

如果我改变参数,即使是低值,例如100:

./perf_event_open.out 100

它给出:

Used 216 instructions

保持那个常数 + 16 条指令,所以看起来准确度相当高,那 16 条一定只是我们小循环之后的 ioctl 设置指令。

现在您可能还对以下内容感兴趣:

可以通过此系统调用测量的其他感兴趣的事件:

在 Ubuntu 20.04 amd64、GCC 9.3.0、Linux 内核 5.4.0、Intel Core i7-7820HQ CPU 上测试。

【讨论】:

  • 当我运行它时,我得到:“打开领导者 1 时出错”。这需要root权限吗?我检查了 perf_event_open 的文档,但似乎并非如此,但我可能遗漏了一些东西。
  • @AlexSpurling 我刚刚在 Ubuntu 20.10 + 与答案中提到的相同硬件上重新运行,它在没有 sudo 的情况下工作。因此,要么您缺少一些内核配置,要么存在一些硬件支持问题。你的发行版 + 确切的 CPU 型号是什么?专门讨论:stackoverflow.com/questions/38442839/…
【解决方案2】:

可能是this question的副本

我说可能是因为您询问了汇编程序指令,但该问题处理的是 C 级代码分析。

但是,我的问题是:为什么要分析实际执行的机器指令?作为第一个问题,这在各种编译器及其优化设置之间会有所不同。作为一个更实际的问题,您实际上可以用这些信息做什么?如果您正在寻找/优化瓶颈,代码分析器就是您要找的。​​p>

不过,我可能会错过一些重要的事情。

【讨论】:

  • 执行的 CPU 指令数将是一种比较算法的简单方法,无需担心打嗝或与其他程序竞争资源,尽管仍依赖于指令集,但与处理能力无关.
  • @mpen:不一定,例如如果您有一个使用大型查找表的算法,而另一个使用更多计算方法执行相同操作的算法,则第一个可能有更多的加载指令,每个可能由于缓存未命中而停滞超过 100 个周期。同样,您可能有一种算法使用大量昂贵的指令,例如FSQRT,以及另一种避免如此昂贵指令并可能使用更多加法/乘法的算法 - 第二种算法可能会更快,即使它执行更多指令。
【解决方案3】:

您可以使用硬件性能计数器 (HPC) 轻松计算执行指令的数量。为了访问 HPC,您需要一个接口。我推荐你使用 PAPI 性能 API。

【讨论】:

  • 你能扩展答案吗?虽然是一个很好的指导,但对于不了解这些技术的人来说,很难知道它到底是什么。
  • @user2316602,今天的处理器配备了称为硬件性能计数器或硬件性能监控单元的特殊寄存器。这些寄存器可以配置为计数微架构事件,如缓存未命中、存储数量、加载指令和已执行指令的数量,也称为退休指令。一些操作系统提供了直接访问这些计数器的接口。我已经进行了许多实验和过程来访问和使用这些计数器。最好的方法是使用 PAPI 基础结构。 PAPI
【解决方案4】:

Intel Pin 的instcount

您可以使用英特尔的二进制检测工具“Pin”。我会避免使用模拟器(它们通常非常慢)。 Pin 完成了您可以在模拟器中完成的大部分工作,而无需重新编译二进制文件并以正常的执行速度(取决于您使用的 pin 工具)。

用 Pin 统计指令数:

  1. 从此处下载最新的(或 3.10,如果此答案过时)pin 套件。
  2. 提取所有内容并转到目录:cd pin-root/source/tools/ManualExample/
  3. 制作目录下的所有工具:make all
  4. 使用以下命令运行名为 inscount0.so 的工具:../../../pin -t obj-intel64/inscount0.so -- your-binary-here
  5. 获取文件inscount.out、cat inscount.out中的指令计数。

输出会是这样的:

➜ ../../../pin -t obj-intel64/inscount0.so -- /bin/ls
buffer_linux.cpp       itrace.cpp
buffer_windows.cpp     little_malloc.c
countreps.cpp          makefile
detach.cpp         makefile.rules
divide_by_zero_unix.c  malloc_mt.cpp
isampling.cpp          w_malloctrace.cpp
➜ cat inscount.out
Count 716372

【讨论】:

    【解决方案5】:

    虽然取决于程序不是“快速”,但可能已经回答了in this question。在这里,Mark Plotnick 建议使用gdb 来观察程序计数器寄存器的变化:

    # instructioncount.gdb
    set pagination off
    set $count=0
    while ($pc != 0xyourstoppingaddress)
        stepi
        set $count++
    end
    print $count
    quit
    

    然后,在您的程序上启动gdb:

    gdb --batch --command instructioncount.gdb --args ./yourexecutable with its arguments
    

    要获取结束地址0xyourstoppingaddress,可以使用以下脚本:

    # stopaddress.gdb
    break main
    run
    info frame
    quit
    

    在函数main上放置一个断点,并给出:

    $ gdb --batch --command stopaddress.gdb --args ./yourexecutable with its arguments
    ...
    Stack level 0, frame at 0x7fffffffdf70:
     rip = 0x40089d in main (main_aes.c:33); saved rip 0x7ffff7a66d20
     source language c.
     Arglist at 0x7fffffffdf60, args: argc=3, argv=0x7fffffffe048
    ...
    

    这里重要的是saved rip 0x7ffff7a66d20 部分。在我的 CPU 上,rip 是指令指针,saved rip 是“返回地址”,如 pepero in this answer 所述。

    所以在这种情况下,停止地址是0x7ffff7a66d20,也就是main函数的返回地址。即程序执行结束。

    【讨论】:

      猜你喜欢
      • 2020-07-07
      • 1970-01-01
      • 2015-01-15
      • 1970-01-01
      • 2018-05-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-14
      相关资源
      最近更新 更多