【问题标题】:Why this `__xstat64` cost this much in perf?为什么这个 `__xstat64` 在性能上要花这么多钱?
【发布时间】:2021-08-23 13:53:25
【问题描述】:

我有一个解析时间戳的 C++ 程序。

#include <iostream>
#include <fstream>
#include <cstdint>
#include <type_traits>
#include <string>
#include <cstddef>
#include <cstring>
#include <sstream>
#include <cstdio>
#include <ctime>


void one(){
    std::ifstream fin;
    fin.open("log2.txt");
    unsigned long long x = 0;
    while (!fin.eof()) {
        std::string line;
        std::getline(fin, line);
        int year;
        int month;
        int day;
        int hour;
        int minute;
        int second;
        int milli_second;
        int timezone_hour;
        int timezone_min;

        std::sscanf(line.data(), "[%d/%d/%d %d:%d:%d.%d %d:%d]", &year, &month, &day, &hour, &minute, &second, &milli_second,
            &timezone_hour, &timezone_min);

        std::tm time {};
        time.tm_year = year - 1900;
        time.tm_mon = month - 1;
        time.tm_mday = day;
        time.tm_hour = hour;
        time.tm_min = minute;
        time.tm_sec = second;

        time_t ctime = mktime(&time) * 1000; 
        x += ctime;
        x %= 192929;
    }
    printf("%llu", x);
    fin.close();
}

int main(int c, char *argv[]){
    bool fi = false;
    printf("c %d\n", c);
    if(c == 2){
        if(argv[1][0] != 'n'){
            fi = true;
            printf("fi\n");
            fflush(stdout);
        }
    }
    if(fi){
        while(1){
            one();
        }
    }else{
        one();
    }
}

数据可以像代码一样生成

#coding: utf8

import time, random, string

def one2(prev_t):
    t = prev_t + random.uniform(1, 100)
    ts = time.strftime("[%Y/%m/%d %H:%M:%S.123 +08:00]", time.localtime(t))
    msg_len = int(random.uniform(2, 20))
    msg = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(msg_len))
    level = random.choice(["DEBUG", "INFO"])
    return t, "{} [{}] [<unknown>] {}\n".format(ts, level, msg)

def make_log():
    t = time.time()
    with open("log3.txt", "w") as f:
        for i in range(1, 10000000):
            t, s = one2(t)
            f.write("{}".format(s))

if __name__ == '__main__':
    make_log()

现在我想用下面的代码来完成它。

g++ -g -O0 t.cpp -o tttzzz
./tttzzz &
ps aux | grep tttzzz
export TPID=$(ps aux | grep 'tttzzz' | grep -v grep | awk '{print $2}')
echo "TPID" $TPID
timeout -s INT 8s perf record -F 99 -e "cpu-clock" -g -p $TPID
perf report

但是,我发现结果是这样的,似乎__xstat64 花费了太多时间。

+   50.89%     0.00%  [.] __xstat64                                                                                          __xstat64+140186421776405       ◆
+   42.20%     0.00%  [k] system_call_fastpath                                                                               system_call_fastpath+18446603336▒
+   41.39%     0.00%  [k] sys_newstat                                                                                        sys_newstat+18446603336221188110▒
+   39.00%     0.00%  [k] SYSC_newstat                                                                                       SYSC_newstat+1844660333622118814▒
+   35.34%     0.00%  [k] vfs_fstatat                                                                                        vfs_fstatat+18446603336221188195▒
+   35.22%     0.00%  [k] user_path_at                                                                                       user_path_at+1844660333622118811▒
+   31.43%     0.00%  [k] user_path_at_empty                                                                                 user_path_at_empty+1844660333622▒
+   31.11%     0.00%  [k] filename_lookup                                                                                    filename_lookup+1844660333622118▒
+   19.80%     0.00%  [.] _IO_vsscanf                                                                                        _IO_vsscanf+140186421776487
+    3.07%     0.00%  [.] main                                                                                               main+4194414

...
     0.72%     0.22%  t        [kernel.kallsyms]    [k] path_get                                                                                             ▒
+    0.72%     0.72%  t        [kernel.kallsyms]    [k] generic_fillattr                                                                                     ▒
     0.69%     0.69%  t        [kernel.kallsyms]    [k] copy_user_enhanced_fast_string                                                                       ▒
+    0.69%     0.00%  t        libc-2.17.so         [.] __GI___libc_read                                                                                     ▒
+    0.67%     0.00%  t        [unknown]            [.] 0x00007fff4122a180                                                                                   ▒
     0.67%     0.67%  t        libc-2.17.so         [.] parse_tzname                                                                                         ▒
+    0.66%     0.66%  t        t                    [.] one    

我的问题是:

  1. __xstat64 在哪里调用?我认为它一定是 mainone 之类的函数的子级?
  2. 为什么这个功能成本这么高,而主要成本只有 3%?

【问题讨论】:

  • stat 可能被称为std::ifstream::open 的实现细节
  • @CoryKramer 但是为什么它不属于main?在this 帖子中,main 的孩子花费了 99% 以上的时间
  • 随机猜测:编译器已将 one 内联到您的 main 函数中。由于您一遍又一遍地调用one,因此文件的内容在内存缓存中是理所当然的,但是操作系统仍然需要进行系统调用(stat)来验证文件是否仍然存在。跨度>
  • pastebin 和类似的链接不足以解决有关代码的 SO 问题。在问题中使用代码块。如果你想显示它正在运行,一个指向godbolt.org 以及的链接作为代码块可能很好。 (Godbolt allows non-shortened "full links" which insure against link rot - 特别是在链接不是 SO 帖子中已经存在的代码的简单复制/粘贴时使用它。)
  • 当我运行 "strace ./tttzzz" 时,我发现了大量的日志,例如 "stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=3519, ...}) = 0",我认为它是由 mktime() 调用来生成本地时间的。

标签: c++ performance perf


【解决方案1】:

本质上,mktime 是瓶颈:

将本地日历时间转换为自纪元以来的时间作为 time_t 对象

从本地日历时间转换为 Unix 纪元是一项不平凡的任务。在 Linux 上,至少在使用 gcc 编译时,它是由 libc 实现的。要了解本地时间在您的机器上的含义,它使用文件/etc/localtime。这就是statsystem 调用出现在strace 中的原因。

您的 strace 输出可能会有所不同,但这是我在使用 gcc 11.1.0 编译后在 Arch Linux 上得到的:

...
newfstatat(AT_FDCWD, "/etc/localtime", {st_mode=S_IFREG|0644, st_size=2298, ...}, 0) = 0
newfstatat(AT_FDCWD, "/etc/localtime", {st_mode=S_IFREG|0644, st_size=2298, ...}, 0) = 0
newfstatat(AT_FDCWD, "/etc/localtime", {st_mode=S_IFREG|0644, st_size=2298, ...}, 0) = 0
... (repeating, as each call of "one" calls mktime, which triggers it)

这应该可以解释为什么__xstat64 主导分析输出。请注意,stat 是一个系统调用,您将控制权交给内核。太贵了。

附带说明,如果您知道时间戳是 UTC,那么将其转换为 Unix 纪元是一项更简单的任务。然而,对于一般当地时间来说,这很棘手,因为实现必须意识到像 daylight savings time 这样的特殊情况。

为什么这个功能成本这么高,而主要成本只有 3%?

Perf 使用采样和only counts the function that is on top of the stackmain 中的 3% 表示,3% 的样本顶部有 main。在one 的执行过程中记录了大部分样本。因此,一些分析工具也会将main 视为热门。然而,默认情况下,perf 只认为 one 是热的,而不是它的调用者。

【讨论】:

  • 好的,有没有什么方法可以让调用者也计算其被调用者的热时间?我觉得会是更直接的输出?
  • @calvin perf 有很多选项,也许“--call-graph”会为您提供您期望的输出。例如,“perf report -g 'graph,0.5,caller'”
  • 我试过了,但我仍然发现 __xstat64__lll_lock_wait_private 没有它的调用者。我使用perf record -F 99 -g -p $TPID --call-graph=dwarf,65528 sleep 8perf report -g 'graph,0.5,caller
猜你喜欢
  • 1970-01-01
  • 2021-07-30
  • 1970-01-01
  • 1970-01-01
  • 2022-01-19
  • 1970-01-01
  • 1970-01-01
  • 2017-11-15
  • 1970-01-01
相关资源
最近更新 更多