Khảo sát kỹ thuật định hình C ++
Trong câu trả lời này, tôi sẽ sử dụng một số công cụ khác nhau để phân tích một vài chương trình kiểm tra rất đơn giản, để so sánh cụ thể cách thức các công cụ đó hoạt động.
Chương trình kiểm tra sau rất đơn giản và thực hiện như sau:
main
các cuộc gọi fast
và maybe_slow
3 lần, một trong nhữngmaybe_slow
cuộc gọi bị chậm
Cuộc gọi chậm maybe_slow
dài hơn gấp 10 lần và chi phối thời gian chạy nếu chúng ta xem xét các cuộc gọi đến chức năng con common
. Lý tưởng nhất, công cụ định hình sẽ có thể chỉ cho chúng ta cuộc gọi chậm cụ thể.
cả hai fast
và maybe_slow
cuộc gọi common
, chiếm phần lớn trong việc thực hiện chương trình
Giao diện chương trình là:
./main.out [n [seed]]
và chương trình thực hiện O(n^2)
các vòng lặp trong tổng số. seed
chỉ để có được đầu ra khác nhau mà không ảnh hưởng đến thời gian chạy.
C chính
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
uint64_t __attribute__ ((noinline)) common(uint64_t n, uint64_t seed) {
for (uint64_t i = 0; i < n; ++i) {
seed = (seed * seed) - (3 * seed) + 1;
}
return seed;
}
uint64_t __attribute__ ((noinline)) fast(uint64_t n, uint64_t seed) {
uint64_t max = (n / 10) + 1;
for (uint64_t i = 0; i < max; ++i) {
seed = common(n, (seed * seed) - (3 * seed) + 1);
}
return seed;
}
uint64_t __attribute__ ((noinline)) maybe_slow(uint64_t n, uint64_t seed, int is_slow) {
uint64_t max = n;
if (is_slow) {
max *= 10;
}
for (uint64_t i = 0; i < max; ++i) {
seed = common(n, (seed * seed) - (3 * seed) + 1);
}
return seed;
}
int main(int argc, char **argv) {
uint64_t n, seed;
if (argc > 1) {
n = strtoll(argv[1], NULL, 0);
} else {
n = 1;
}
if (argc > 2) {
seed = strtoll(argv[2], NULL, 0);
} else {
seed = 0;
}
seed += maybe_slow(n, seed, 0);
seed += fast(n, seed);
seed += maybe_slow(n, seed, 1);
seed += fast(n, seed);
seed += maybe_slow(n, seed, 0);
seed += fast(n, seed);
printf("%" PRIX64 "\n", seed);
return EXIT_SUCCESS;
}
gprof
gprof yêu cầu biên dịch lại phần mềm với thiết bị và nó cũng sử dụng phương pháp lấy mẫu cùng với thiết bị đó. Do đó, nó tạo ra sự cân bằng giữa độ chính xác (lấy mẫu không phải lúc nào cũng hoàn toàn chính xác và có thể bỏ qua các chức năng) và làm chậm thực thi (thiết bị và lấy mẫu là các kỹ thuật tương đối nhanh, không làm chậm quá trình thực thi).
gprof được tích hợp GCC / binutils, vì vậy tất cả những gì chúng ta phải làm là biên dịch với -pg
tùy chọn bật gprof. Sau đó, chúng tôi chạy chương trình bình thường với tham số CLI kích thước tạo ra thời gian chạy hợp lý trong vài giây ( 10000
):
gcc -pg -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time ./main.out 10000
Vì lý do giáo dục, chúng tôi cũng sẽ chạy mà không kích hoạt tối ưu hóa. Lưu ý rằng điều này là vô ích trong thực tế, vì thông thường bạn chỉ quan tâm đến việc tối ưu hóa hiệu suất của chương trình được tối ưu hóa:
gcc -pg -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
./main.out 10000
Đầu tiên, time
cho chúng ta biết rằng thời gian thực hiện có và không -pg
giống nhau, điều này thật tuyệt: không bị chậm lại! Tuy nhiên, tôi đã thấy các tài khoản giảm tốc độ 2x - 3x trên phần mềm phức tạp, ví dụ như được hiển thị trong vé này .
Bởi vì chúng tôi đã biên dịch -pg
, chạy chương trình sẽ tạo ra một tệpgmon.out
tệp chứa dữ liệu lược tả.
Chúng ta có thể quan sát tệp đó bằng đồ họa với câu gprof2dot
hỏi tại: Có thể lấy biểu diễn đồ họa của kết quả gprof không?
sudo apt install graphviz
python3 -m pip install --user gprof2dot
gprof main.out > main.gprof
gprof2dot < main.gprof | dot -Tsvg -o output.svg
Tại đây, gprof
công cụ đọc gmon.out
thông tin theo dõi và tạo một báo cáo có thể đọc được trong main.gprof
đógprof2dot
sau đó đọc để tạo biểu đồ.
Nguồn cho gprof2dot là tại: https://github.com/jrfonseca/gprof2dot
Chúng tôi quan sát sau đây để -O0
chạy:
và để -O3
chạy:
Đầu -O0
ra là khá nhiều tự giải thích. Ví dụ, nó cho thấy rằng 3 maybe_slow
cuộc gọi và các cuộc gọi con của chúng chiếm 97,56% tổng thời gian chạy, mặc dù việc thực hiện maybe_slow
mà không có con chiếm 0,00% tổng thời gian thực hiện, tức là gần như toàn bộ thời gian dành cho chức năng đó đã dành cho con gọi.
TODO: tại sao main
bị thiếu từ -O3
đầu ra, mặc dù tôi có thể thấy nó trên một bt
GDB? Thiếu chức năng từ đầu ra GProf Tôi nghĩ rằng đó là do gprof cũng lấy mẫu dựa trên công cụ được biên dịch của nó, và -O3
main
nó quá nhanh và không có mẫu.
Tôi chọn đầu ra SVG thay vì PNG vì SVG có thể tìm kiếm được bằng Ctrl + F và kích thước tệp có thể nhỏ hơn khoảng 10 lần. Ngoài ra, chiều rộng và chiều cao của hình ảnh được tạo ra có thể rất khiêm tốn với hàng chục nghìn pixel cho phần mềm phức tạp và eog
lỗi Gnome 3.28.1 trong trường hợp đó đối với PNG, trong khi SVG được trình duyệt của tôi tự động mở. gimp 2.8 hoạt động tốt mặc dù, xem thêm:
nhưng ngay cả khi đó, bạn sẽ kéo hình ảnh xung quanh rất nhiều để tìm thấy những gì bạn muốn, xem ví dụ hình ảnh này từ một ví dụ phần mềm "thực" được lấy từ vé này :
Bạn có thể tìm thấy ngăn xếp cuộc gọi quan trọng nhất một cách dễ dàng với tất cả các dòng spaghetti nhỏ chưa được sắp xếp này đi qua nhau không? Có thể có những dot
lựa chọn tốt hơn tôi chắc chắn, nhưng tôi không muốn đến đó ngay bây giờ. Những gì chúng tôi thực sự cần là một người xem dành riêng cho nó, nhưng tôi chưa tìm thấy:
Tuy nhiên, bạn có thể sử dụng bản đồ màu để giảm thiểu những vấn đề đó một chút. Ví dụ, trên hình ảnh lớn trước đó, cuối cùng tôi đã tìm được con đường quan trọng ở bên trái khi tôi đưa ra suy luận tuyệt vời rằng màu xanh lá cây xuất hiện sau màu đỏ, cuối cùng là màu xanh đậm hơn và tối hơn.
Ngoài ra, chúng ta cũng có thể quan sát đầu ra văn bản của gprof
công cụ binutils tích hợp mà trước đây chúng ta đã lưu tại:
cat main.gprof
Theo mặc định, điều này tạo ra một đầu ra cực kỳ dài để giải thích ý nghĩa của dữ liệu đầu ra. Vì tôi không thể giải thích tốt hơn thế, tôi sẽ để bạn tự đọc.
Khi bạn đã hiểu định dạng đầu ra dữ liệu, bạn có thể giảm mức độ chi tiết để chỉ hiển thị dữ liệu mà không có hướng dẫn với -b
tùy chọn:
gprof -b main.out
Trong ví dụ của chúng tôi, các kết quả đầu ra là cho -O0
:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
100.35 3.67 3.67 123003 0.00 0.00 common
0.00 3.67 0.00 3 0.00 0.03 fast
0.00 3.67 0.00 3 0.00 1.19 maybe_slow
Call graph
granularity: each sample hit covers 2 byte(s) for 0.27% of 3.67 seconds
index % time self children called name
0.09 0.00 3003/123003 fast [4]
3.58 0.00 120000/123003 maybe_slow [3]
[1] 100.0 3.67 0.00 123003 common [1]
-----------------------------------------------
<spontaneous>
[2] 100.0 0.00 3.67 main [2]
0.00 3.58 3/3 maybe_slow [3]
0.00 0.09 3/3 fast [4]
-----------------------------------------------
0.00 3.58 3/3 main [2]
[3] 97.6 0.00 3.58 3 maybe_slow [3]
3.58 0.00 120000/123003 common [1]
-----------------------------------------------
0.00 0.09 3/3 main [2]
[4] 2.4 0.00 0.09 3 fast [4]
0.09 0.00 3003/123003 common [1]
-----------------------------------------------
Index by function name
[1] common [4] fast [3] maybe_slow
và cho -O3
:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls us/call us/call name
100.52 1.84 1.84 123003 14.96 14.96 common
Call graph
granularity: each sample hit covers 2 byte(s) for 0.54% of 1.84 seconds
index % time self children called name
0.04 0.00 3003/123003 fast [3]
1.79 0.00 120000/123003 maybe_slow [2]
[1] 100.0 1.84 0.00 123003 common [1]
-----------------------------------------------
<spontaneous>
[2] 97.6 0.00 1.79 maybe_slow [2]
1.79 0.00 120000/123003 common [1]
-----------------------------------------------
<spontaneous>
[3] 2.4 0.00 0.04 fast [3]
0.04 0.00 3003/123003 common [1]
-----------------------------------------------
Index by function name
[1] common
Như một bản tóm tắt rất nhanh cho mỗi phần, ví dụ:
0.00 3.58 3/3 main [2]
[3] 97.6 0.00 3.58 3 maybe_slow [3]
3.58 0.00 120000/123003 common [1]
trung tâm xung quanh hàm được thụt lề ( maybe_flow
). [3]
là ID của chức năng đó. Phía trên chức năng, là những người gọi của nó, và bên dưới nó là các callees.
Đối với -O3
, hãy xem ở đây như trong đầu ra đồ họa maybe_slow
và fast
không có cha mẹ đã biết, đó là những gì tài liệu nói lên <spontaneous>
.
Tôi không chắc có cách nào để thực hiện hồ sơ theo từng dòng với gprof: `gprof` dành thời gian cho các dòng mã cụ thể
cuộc gọi valgrind
valgrind chạy chương trình thông qua máy ảo valgrind. Điều này làm cho hồ sơ rất chính xác, nhưng nó cũng tạo ra một sự chậm lại rất lớn của chương trình. Tôi cũng đã đề cập đến kcachegrind trước đây tại: Công cụ để có được một biểu đồ gọi hàm mã hình ảnh của mã
callgrind là công cụ của valgrind để mã hồ sơ và kcachegrind là một chương trình KDE có thể trực quan hóa đầu ra của bộ nhớ cache.
Đầu tiên chúng ta phải xóa -pg
cờ để quay lại quá trình biên dịch bình thường, nếu không thì việc chạy thực sự thất bại vớiProfiling timer expired
, và vâng, điều này phổ biến đến mức tôi đã làm và có một câu hỏi Stack Overflow cho nó.
Vì vậy, chúng tôi biên dịch và chạy như:
sudo apt install kcachegrind valgrind
gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
time valgrind --tool=callgrind valgrind --dump-instr=yes \
--collect-jumps=yes ./main.out 10000
Tôi kích hoạt --dump-instr=yes --collect-jumps=yes
bởi vì điều này cũng loại bỏ thông tin cho phép chúng tôi xem phân tích hiệu suất trên mỗi dây chuyền lắp ráp, với chi phí bổ sung tương đối nhỏ.
Tắt dơi, time
nói với chúng tôi rằng chương trình mất 29,5 giây để thực thi, vì vậy chúng tôi đã làm chậm khoảng 15 lần trong ví dụ này. Rõ ràng, sự chậm lại này sẽ là một hạn chế nghiêm trọng cho khối lượng công việc lớn hơn. Trên "ví dụ phần mềm thế giới thực" được đề cập ở đây , tôi đã quan sát thấy sự chậm lại 80 lần.
Việc chạy tạo ra một tệp dữ liệu hồ sơ có tên callgrind.out.<pid>
ví dụ callgrind.out.8554
trong trường hợp của tôi. Chúng tôi xem tập tin đó với:
kcachegrind callgrind.out.8554
trong đó hiển thị GUI chứa dữ liệu tương tự như đầu ra gprof văn bản:
Ngoài ra, nếu chúng ta đi vào tab "Biểu đồ cuộc gọi" phía dưới bên phải, chúng ta sẽ thấy một biểu đồ cuộc gọi mà chúng ta có thể xuất bằng cách nhấp chuột phải vào nó để có được hình ảnh sau với số lượng viền trắng không hợp lý :-)
Tôi nghĩ rằng fast
không hiển thị trên biểu đồ đó vì kcachegrind phải đơn giản hóa việc trực quan hóa vì cuộc gọi đó chiếm quá ít thời gian, đây có thể sẽ là hành vi bạn muốn trên một chương trình thực. Menu nhấp chuột phải có một số cài đặt để kiểm soát thời điểm loại bỏ các nút như vậy, nhưng tôi không thể làm cho nó hiển thị một cuộc gọi ngắn như vậy sau một nỗ lực nhanh chóng. Nếu tôi nhấp vào fast
cửa sổ bên trái, nó sẽ hiển thị biểu đồ cuộc gọi fast
, để ngăn xếp đó thực sự được ghi lại. Chưa ai tìm được cách hiển thị biểu đồ cuộc gọi đồ thị hoàn chỉnh: Thực hiện cuộc gọi hiển thị tất cả các cuộc gọi chức năng trong biểu đồ cuộc gọi kcachegrind
TODO trên phần mềm C ++ phức tạp, tôi thấy một số mục thuộc loại <cycle N>
, ví dụ: <cycle 11>
nơi tôi mong đợi tên hàm, điều đó có nghĩa là gì? Tôi nhận thấy có một nút "Phát hiện chu kỳ" để bật và tắt, nhưng nó có nghĩa là gì?
perf
từ linux-tools
perf
dường như sử dụng các cơ chế lấy mẫu nhân Linux độc quyền. Điều này làm cho nó rất đơn giản để thiết lập, nhưng cũng không hoàn toàn chính xác.
sudo apt install linux-tools
time perf record -g ./main.out 10000
Điều này đã thêm 0,2 giây để thực thi, vì vậy chúng tôi rất khôn ngoan về thời gian, nhưng tôi vẫn không thấy nhiều sự quan tâm, sau khi mở rộng common
nút bằng mũi tên phải của bàn phím:
Samples: 7K of event 'cycles:uppp', Event count (approx.): 6228527608
Children Self Command Shared Object Symbol
- 99.98% 99.88% main.out main.out [.] common
common
0.11% 0.11% main.out [kernel] [k] 0xffffffff8a6009e7
0.01% 0.01% main.out [kernel] [k] 0xffffffff8a600158
0.01% 0.00% main.out [unknown] [k] 0x0000000000000040
0.01% 0.00% main.out ld-2.27.so [.] _dl_sysdep_start
0.01% 0.00% main.out ld-2.27.so [.] dl_main
0.01% 0.00% main.out ld-2.27.so [.] mprotect
0.01% 0.00% main.out ld-2.27.so [.] _dl_map_object
0.01% 0.00% main.out ld-2.27.so [.] _xstat
0.00% 0.00% main.out ld-2.27.so [.] __GI___tunables_init
0.00% 0.00% main.out [unknown] [.] 0x2f3d4f4944555453
0.00% 0.00% main.out [unknown] [.] 0x00007fff3cfc57ac
0.00% 0.00% main.out ld-2.27.so [.] _start
Vì vậy, sau đó tôi cố gắng điểm chuẩn -O0
chương trình để xem nếu điều đó hiển thị bất cứ điều gì, và chỉ bây giờ, cuối cùng, tôi mới thấy một biểu đồ cuộc gọi:
Samples: 15K of event 'cycles:uppp', Event count (approx.): 12438962281
Children Self Command Shared Object Symbol
+ 99.99% 0.00% main.out [unknown] [.] 0x04be258d4c544155
+ 99.99% 0.00% main.out libc-2.27.so [.] __libc_start_main
- 99.99% 0.00% main.out main.out [.] main
- main
- 97.54% maybe_slow
common
- 2.45% fast
common
+ 99.96% 99.85% main.out main.out [.] common
+ 97.54% 0.03% main.out main.out [.] maybe_slow
+ 2.45% 0.00% main.out main.out [.] fast
0.11% 0.11% main.out [kernel] [k] 0xffffffff8a6009e7
0.00% 0.00% main.out [unknown] [k] 0x0000000000000040
0.00% 0.00% main.out ld-2.27.so [.] _dl_sysdep_start
0.00% 0.00% main.out ld-2.27.so [.] dl_main
0.00% 0.00% main.out ld-2.27.so [.] _dl_lookup_symbol_x
0.00% 0.00% main.out [kernel] [k] 0xffffffff8a600158
0.00% 0.00% main.out ld-2.27.so [.] mmap64
0.00% 0.00% main.out ld-2.27.so [.] _dl_map_object
0.00% 0.00% main.out ld-2.27.so [.] __GI___tunables_init
0.00% 0.00% main.out [unknown] [.] 0x552e53555f6e653d
0.00% 0.00% main.out [unknown] [.] 0x00007ffe1cf20fdb
0.00% 0.00% main.out ld-2.27.so [.] _start
TODO: chuyện gì đã xảy ra trong vụ -O3
hành quyết? Có phải chỉ đơn giản là như vậy maybe_slow
và fast
quá nhanh và không nhận được bất kỳ mẫu nào? Nó có hoạt động tốt với -O3
các chương trình lớn hơn mất nhiều thời gian hơn để thực hiện không? Tôi đã bỏ lỡ một số tùy chọn CLI? Tôi phát hiện ra về -F
việc kiểm soát tần số mẫu trong Hertz, nhưng tôi đã bật nó lên mức tối đa được phép theo mặc định -F 39500
(có thể tăng lên sudo
) và tôi vẫn không thấy các cuộc gọi rõ ràng.
Một điều thú vị perf
là công cụ FlameGraph của Brendan Gregg, hiển thị thời gian của ngăn xếp cuộc gọi một cách rất gọn gàng cho phép bạn nhanh chóng xem các cuộc gọi lớn. Công cụ có sẵn tại địa chỉ: https://github.com/brendangregg/FlameGraph và cũng được đề cập trên mình Perf hướng dẫn tại địa chỉ: http://www.brendangregg.com/perf.html#FlameGraphs Khi tôi chạy perf
mà không cần sudo
tôi ERROR: No stack counts found
như vậy cho bây giờ tôi sẽ làm điều đó với sudo
:
git clone https://github.com/brendangregg/FlameGraph
sudo perf record -F 99 -g -o perf_with_stack.data ./main.out 10000
sudo perf script -i perf_with_stack.data | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > flamegraph.svg
nhưng trong một chương trình đơn giản như vậy, đầu ra không dễ hiểu lắm, vì chúng ta không thể dễ dàng nhìn thấy maybe_slow
cũng như fast
trên biểu đồ đó:
Trong một ví dụ phức tạp hơn, nó trở nên rõ ràng về ý nghĩa của biểu đồ:
TODO có một bản ghi của [unknown]
hàm trong ví dụ đó, tại sao vậy?
Một giao diện GUI hoàn hảo khác có thể đáng giá bao gồm:
Plugin Eclipse Trace Compass: https://www.eclipse.org/tracecompass/
Nhưng điều này có nhược điểm là trước tiên bạn phải chuyển đổi dữ liệu sang Định dạng theo dõi chung, có thể được thực hiện perf data --to-ctf
, nhưng nó cần phải được bật khi xây dựng / có perf
đủ mới, một trong hai trường hợp không phải là hoàn hảo. Ubuntu 18.04
https://github.com/KDAB/hotspot
Nhược điểm của điều này là dường như không có gói Ubuntu và việc xây dựng nó đòi hỏi Qt 5.10 trong khi Ubuntu 18.04 ở mức Qt 5.9.
gperftools
Trước đây được gọi là "Công cụ hiệu suất của Google", nguồn: https://github.com/gperftools/gperftools Mẫu dựa trên.
Đầu tiên cài đặt gperftools với:
sudo apt install google-perftools
Sau đó, chúng ta có thể kích hoạt trình cấu hình CPU gperftools theo hai cách: tại thời gian chạy hoặc tại thời gian xây dựng.
Trong thời gian chạy, chúng ta phải vượt qua thiết lập LD_PRELOAD
để trỏ tới libprofiler.so
, mà bạn có thể tìm thấy locate libprofiler.so
, ví dụ như trên hệ thống của tôi:
gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libprofiler.so \
CPUPROFILE=prof.out ./main.out 10000
Ngoài ra, chúng ta có thể xây dựng thư viện tại thời điểm liên kết, phân phối truyền LD_PRELOAD
khi chạy:
gcc -Wl,--no-as-needed,-lprofiler,--as-needed -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
CPUPROFILE=prof.out ./main.out 10000
Xem thêm: gperftools - tập tin hồ sơ không bị đổ
Cách tốt nhất để xem dữ liệu này tôi đã tìm thấy cho đến nay là làm cho đầu ra pprof có cùng định dạng mà kcachegrind lấy làm đầu vào (vâng, công cụ Valgrind-project-viewer-viewer) và sử dụng kcachegrind để xem:
google-pprof --callgrind main.out prof.out > callgrind.out
kcachegrind callgrind.out
Sau khi chạy với một trong các phương thức đó, chúng tôi nhận được prof.out
tệp dữ liệu hồ sơ làm đầu ra. Chúng ta có thể xem tệp đó dưới dạng SVG với:
google-pprof --web main.out prof.out
cung cấp dưới dạng biểu đồ cuộc gọi quen thuộc như các công cụ khác, nhưng với đơn vị số lượng mẫu không rõ ràng hơn là giây.
Ngoài ra, chúng tôi cũng có thể nhận được một số dữ liệu văn bản với:
google-pprof --text main.out prof.out
cung cấp cho:
Using local file main.out.
Using local file prof.out.
Total: 187 samples
187 100.0% 100.0% 187 100.0% common
0 0.0% 100.0% 187 100.0% __libc_start_main
0 0.0% 100.0% 187 100.0% _start
0 0.0% 100.0% 4 2.1% fast
0 0.0% 100.0% 187 100.0% main
0 0.0% 100.0% 183 97.9% maybe_slow
Xem thêm: Cách sử dụng google perf tools
Đã thử nghiệm trong Ubuntu 18.04, gprof2dot 2019.11.30, valgrind 3.13.0, perf 4.15.18, Linux kernel 4.15.0, FLameGraph 1a0dc6985aad06e76857cf2a354bd5ba0c9ce96b, gperftools 2.5-2.