gpt4 book ai didi

c++ - 当性能至关重要时,如何编写Linux C++调试信息?

转载 作者:行者123 更新时间:2023-11-30 03:43:10 25 4
gpt4 key购买 nike

我正在尝试调试带有许多变量的大型程序。通过以下方式设置代码:

while (condition1) {
//non timing sensitive code
while (condition2) {
//timing sensitive code
//many variables that change each iteration
}
}

我想保存供查看的内循环上有很多变量。我想在每个外循环迭代中将它们写入文本文件。内部循环在每次迭代中执行不同的次数。它可以是2或3,也可以是几千。

我需要查看每个内部迭代中的所有变量值,但是我需要保持内部循环尽可能快。

最初,我只是尝试将每个数据变量存储在自己的 vector 中,在每个内部循环迭代中都附加一个值。然后,当外循环迭代到来时,我将从 vector 中读取数据并将数据写入调试文件。随着变量的添加,这很快就失控了。

我考虑过使用字符串缓冲区来存储信息,但是我不确定这是否是最快的方法,因为在循环中需要多次创建给定的字符串。另外,由于我不知道迭代次数,所以我不确定缓冲区会增长到多大。

存储的信息采用以下格式:
"Var x: 10\n
Var y: 20\n
.
.
.
Other Text: Stuff\n"

那么,有没有更干净的选择可以快速写入大量调试数据呢?

最佳答案

如果它确实对时间敏感,则不要在关键循环内格式化字符串。

我会在关键循环内将记录追加到二进制记录的日志缓冲区中。外循环可以将其直接写入二进制文件(可以稍后处理),也可以基于记录格式化文本。

这样做的优点是,循环仅需要跟踪几个额外的变量(指向一个std::vector的已用空间和已分配空间的末尾的指针),而不是为每个记录的变量跟踪一个std::vector的指针。这将对关键循环中的寄存器分配产生较小的影响。

在我的测试中,看起来您只是获得了一些额外的循环开销来跟踪 vector ,并为要记录的每个变量提供了一条存储指令。我没有编写足够大的测试循环来暴露从保持所有变量“有效”到emplace_back()为止的任何潜在问题。如果编译器在需要溢出寄存器的较大循环方面做得不好,请参阅以下有关使用简单数组而不进行任何大小检查的部分。那应该消除对编译器的任何约束,使编译器尝试同时将所有存储都存储到日志缓冲区中。

这是我建议的示例。它编译并运行,编写一个二进制日志文件,您可以对其进行十六进制转储。

Godbolt compiler explorer上以良好的格式查看源和asm输出。它甚至可以为源代码和汇编代码着色,因此您可以更轻松地查看哪个汇编代码来自哪个源代码行。

#include <vector>
#include <cstdint>
#include <cstddef>
#include <iostream>

struct loop_log {
// Generally sort in order of size for better packing.
// Use as narrow types as possible to reduce memory bandwidth.
// e.g. logging an int loop counter into a short log record is fine if you're sure it always in-practice fits in a short, and has zero performance downside
int64_t x, y, z;
uint64_t ux, uy, uz;
int32_t a, b, c;
uint16_t t, i, j;
uint8_t c1, c2, c3;
// isn't there a less-repetitive way to write this?
loop_log(int64_t x, int32_t a, int outer_counter, char c1)
: x(x), a(a), i(outer_counter), c1(c1)
// leaves other members *uninitialized*, not zeroed.
// note lack of gcc warning for initializing uint16_t i from an int
// and for not mentioning every member
{}
};


static constexpr size_t initial_reserve = 10000;

// take some args so gcc can't count the iterations at compile time
void foo(std::ostream &logfile, int outer_iterations, int inner_param) {
std::vector<struct loop_log> log;
log.reserve(initial_reserve);
int outer_counter = outer_iterations;

while (--outer_counter) {
//non timing sensitive code
int32_t a = inner_param - outer_counter;
while (a != 0) {
//timing sensitive code
a <<= 1;
int64_t x = outer_counter * (100LL + a);
char c1 = x;

// much more efficient code with gcc 5.3 -O3 than push_back( a struct literal );
log.emplace_back(x, a, outer_counter, c1);
}
const auto logdata = log.data();
const size_t bytes = log.size() * sizeof(*logdata);
// write group size, then a group of records
logfile.write( reinterpret_cast<const char *>(&bytes), sizeof(bytes) );
logfile.write( reinterpret_cast<const char *>(logdata), bytes );
// you could format the records into strings at this point if you want
log.clear();
}
}


#include <fstream>
int main() {
std::ofstream logfile("dbg.log");
foo(logfile, 100, 10);
}

gcc的 foo()输出几乎可以优化所有 vector 开销。只要初始 reserve()足够大,内部循环就是:
## gcc 5.3 -masm=intel  -O3 -march=haswell -std=gnu++11 -fverbose-asm
## The inner loop from the above C++:
.L59:
test rbx, rbx # log // IDK why gcc wants to check for a NULL pointer inside the hot loop, instead of doing it once after reserve() calls new()
je .L6 #,
mov QWORD PTR [rbx], rbp # log_53->x, x // emplace_back the 4 elements
mov DWORD PTR [rbx+48], r12d # log_53->a, a
mov WORD PTR [rbx+62], r15w # log_53->i, outer_counter
mov BYTE PTR [rbx+66], bpl # log_53->c1, x
.L6:
add rbx, 72 # log, // struct size is 72B
mov r8, r13 # D.44727, log
test r12d, r12d # a
je .L58 #, // a != 0
.L4:
add r12d, r12d # a // a <<= 1
movsx rbp, r12d # D.44726, a // x = ...
add rbp, 100 # D.44726, // x = ...
imul rbp, QWORD PTR [rsp+8] # x, %sfp // x = ...
cmp r14, rbx # log$D40277$_M_impl$_M_end_of_storage, log
jne .L59 #, // stay in this tight loop as long as we don't run out of reserved space in the vector

// fall through into code that allocates more space and copies.
// gcc generates pretty lame copy code, using 8B integer loads/stores, not rep movsq. Clang uses AVX to copy 32B at a time
// anyway, that code never runs as long as the reserve is big enough
// I guess std::vector doesn't try to realloc() to avoid the copy if possible (e.g. if the following virtual address region is unused) :/

尝试避免重复的构造函数代码:

我尝试了使用支撑初始化列表的版本,以避免必须编写真正重复的构造函数,但是从gcc获得了更糟糕的代码:
#ifdef USE_CONSTRUCTOR
// much more efficient code with gcc 5.3 -O3.
log.emplace_back(x, a, outer_counter, c1);
#else
// Put the mapping from local var names to struct member names right here in with the loop
log.push_back( (struct loop_log) {
.x = x, .y =0, .z=0, // C99 designated-initializers are a GNU extension to C++,
.ux=0, .uy=0, .uz=0, // but gcc doesn't support leaving having uninitialized elements before the last initialized one:
.a = a, .b=0, .c=0, // without all the ...=0, you get "sorry, unimplemented: non-trivial designated initializers not supported"
.t=0, .i = outer_counter, .j=0,
.c1 = (uint8_t)c1
} );
#endif

不幸的是,这将一个结构存储到堆栈上,然后一次使用以下代码将其复制8B:
    mov     rax, QWORD PTR [rsp+72]
mov QWORD PTR [rdx+8], rax // rdx points into the vector's buffer
mov rax, QWORD PTR [rsp+80]
mov QWORD PTR [rdx+16], rax
... // total of 9 loads/stores for a 72B struct

因此,它将对内循环产生更大的影响。

push_back() a struct into a vector有几种方法,但是不幸的是,使用braced-initializer-list似乎总会导致gcc 5.3无法优化副本。最好避免为构造函数编写很多重复的代码。使用指定的初始值设定项列表( {.x = val}),循环内的代码不必在乎结构实际存储事物的顺序。您可以按照易于阅读的顺序编写它们。

顺便说一句, .x= val C99指定的初始化程序语法是C++的GNU扩展。另外,您会收到警告,提示您忘记使用gcc的 -Wextra(启用 -Wmissing-field-initializers)初始化括号列表中的成员。

有关初始化程序语法的更多信息,请查看 Brace-enclosed initializer list constructorthe docs for member initialization

这是一个有趣但可怕的想法:
// Doesn't compiler.  Worse: hard to read, probably easy to screw up
while (outerloop) {
int64_t x=0, y=1;
struct loop_log {int64_t logx=x, logy=y;}; // loop vars as default initializers
// error: default initializers can't be local vars with automatic storage.
while (innerloop) { x+=y; y+=x; log.emplace_back(loop_log()); }
}

通过使用平面数组而不是 std::vector来降低开销

也许试图让编译器优化各种 std::vector操作的好处远不只是编写大量结构(静态,本地或动态)并自己计算有效记录的数量。 std::vector会检查您是否在每次迭代中都用完了保留的空间,但是您不需要这样的东西(如果有固定的上限),您可以使用它分配足够的空间以永不溢出。 (取决于平台和您如何分配空间,分配但未写入的大块内存并不是真正的问题。例如,在Linux上,malloc使用 mmap(MAP_ANONYMOUS)进行大分配,这使您的页面全部被复制- on-write映射到已清零的物理页面。在您写入之前,OS不需要分配物理页面。对大型静态数组也应如此。)

因此,在您的循环中,您可能只需编写如下代码
loop_log *current_record = logbuf;
while(inner_loop) {
int64_t x = ...;
current_record->x = x;
...
current_record->i = (short)outer_counter;
...
// or maybe
// *current_record = { .x = x, .i = (short)outer_counter };
// compilers will probably have an easier time avoiding any copying with a braced initializer list in this case than with vector.push_back

current_record++;
}
size_t record_bytes = (current_record - log) * sizeof(log[0]);
// or size_t record_bytes = static_cast<char*>(current_record) - static_cast<char*>(log);
logfile.write((const char*)logbuf, record_bytes);

将存储分散在整个内部循环中将需要数组指针始终处于 Activity 状态,但是OTOH并不需要所有循环变量都处于同时 Activity 状态。如果gcc会优化 emplace_back以在不再需要该变量时将每个变量存储到 vector 中,或者如果它可能将变量溢出到堆栈中,然后在一组指令中将它们全部复制到 vector 中,则为IDK。

使用 log[records++].x = ...可能会导致编译器保留数组和计数器占用两个寄存器,因为我们将在外部循环中使用记录计数。我们希望内部循环更快,并且可以花一些时间在外部循环中进行减法运算,因此我使用指针增量来编写它,以鼓励编译器仅对该状态使用一个寄存器。除了注册压力, base+index store instructions are less efficient on Intel SnB-family hardware than single-register addressing modes

您仍然可以为此使用 std::vector,但是很难让 std::vector不将零写入其分配的内存中。 reserve()只是分配而不会清零,但是您调用 .data()并使用保留的空间而不用 vector告诉 .resize()失败了。当然 .resize()将初始化所有新元素。因此,您 std::vector是获取大量分配而不弄脏它的一个不好的选择。

关于c++ - 当性能至关重要时,如何编写Linux C++调试信息?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36295767/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com