Вопрос: Как я могу создать код на C ++, работающий в Linux?


У меня есть приложение на C ++, работающее на Linux, которое я в процессе оптимизации. Как я могу определить, какие области моего кода работают медленно?


1482


источник


Ответы:


Если ваша цель - использовать профилировщик, используйте один из предложенных.

Однако, если вы спешите, и вы можете вручную прервать свою программу под отладчиком, пока она субъективно медленная, есть простой способ найти проблемы с производительностью.

Просто остановите его несколько раз и каждый раз смотрите на стек вызовов. Если есть какой-то код, который тратит часть процента времени, 20% или 50% или что-то еще, это вероятность того, что вы поймаете его в действии по каждому образцу. Так что это примерно процент образцов, на которых вы его увидите. Не требуется никаких просвещенных догадок. Если у вас есть догадка о том, в чем проблема, это докажет или опровергнет это.

У вас может быть несколько проблем с производительностью разных размеров. Если вы очистите какой-либо из них, оставшиеся возьмут больший процент и будут легче распознать при последующих проходах. Эта эффект увеличения , когда усугубляется несколько проблем, может привести к действительно огромным факторам ускорения.

Предостережение. Программисты склонны скептически относиться к этой методике, если они не использовали ее сами. Они скажут, что профилировщики предоставляют вам эту информацию, но это справедливо только в том случае, если они пробуют весь стек вызовов, а затем позволяют изучить случайный набор выборок. (Резюме - это то, где понимание потеряно.) Графы вызовов не дают вам той же информации, потому что

  1. они не суммируются на уровне инструкций, и
  2. они дают путаные резюме в присутствии рекурсии.

Они также скажут, что он работает только с игрушечными программами, когда на самом деле он работает над любой программой, и, похоже, он работает лучше на больших программах, потому что у них больше проблем, которые можно найти. Они скажут, что это иногда находит вещи, которые не являются проблемами, но это справедливо только в том случае, если вы видите что-то один раз , Если вы видите проблему более чем на одном образце, это реально.

Постскриптум Это также можно сделать в многопоточных программах, если есть способ собрать образцы стека вызовов пула потоков в определенный момент времени, как это происходит в Java.

P.P.S Как грубая общность, чем больше слоев абстракции у вас в вашем программном обеспечении, тем больше вероятность того, что вы обнаружите, что это является причиной проблем с производительностью (и возможность ускорения).

Добавлено: Это может быть не очевидно, но метод выборки в стеке работает одинаково хорошо в присутствии рекурсии. Причина в том, что время, которое было бы спасено путем удаления инструкции, аппроксимируется фракцией содержащихся в ней выборок, независимо от количества раз, которое она может иметь место в выборке.

Еще одно возражение, которое я часто слышу, это: Он остановится где-нибудь случайным, и он упустит реальную проблему ». Это происходит из-за того, что существует предварительная концепция того, что представляет собой настоящая проблема. Ключевым свойством проблем с производительностью является то, что они бросают вызов ожиданиям. Отбор проб говорит вам что-то проблема, и ваша первая реакция - это недоверие. Это естественно, но вы можете быть уверены, что он обнаружил, что проблема реальна, и наоборот.

ДОБАВЛЕНО: Позвольте мне объяснить байесовское объяснение того, как это работает. Предположим, что имеется некоторая инструкция I(вызов или иначе), который находится в стеке вызовов, некоторая доля f(и, следовательно, так много затрат). Для простоты предположим, что мы не знаем, что f, но предположим, что это либо 0,1, 0,2, 0,3, ... 0,9, 1,0, а предыдущая вероятность каждой из этих возможностей равна 0,1, поэтому все эти затраты одинаково вероятны априорно.

Тогда предположим, что мы берем только 2 образца стека, и мы видим инструкцию Iна обоих образцах, обозначенное наблюдение o=2/2, Это дает нам новые оценки частоты fиз I, согласно этому:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&&f=x)  P(o=2/2&&f >= x)  P(f >= x)

0.1    1     1             0.1          0.1            0.25974026
0.1    0.9   0.81          0.081        0.181          0.47012987
0.1    0.8   0.64          0.064        0.245          0.636363636
0.1    0.7   0.49          0.049        0.294          0.763636364
0.1    0.6   0.36          0.036        0.33           0.857142857
0.1    0.5   0.25          0.025        0.355          0.922077922
0.1    0.4   0.16          0.016        0.371          0.963636364
0.1    0.3   0.09          0.009        0.38           0.987012987
0.1    0.2   0.04          0.004        0.384          0.997402597
0.1    0.1   0.01          0.001        0.385          1

                  P(o=2/2) 0.385                

В последнем столбце говорится, что, например, вероятность того, что f> = 0,5 составляет 92%, по сравнению с предшествующим допущением 60%.

Предположим, что предыдущие предположения различны. Предположим, что P (f = 0,1) составляет 0,991 (почти наверняка), а все остальные возможности практически невозможны (0,001). Другими словами, наша предварительная уверенность в том, что Iдешево. Тогда получим:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&& f=x)  P(o=2/2&&f >= x)  P(f >= x)

0.001  1    1              0.001        0.001          0.072727273
0.001  0.9  0.81           0.00081      0.00181        0.131636364
0.001  0.8  0.64           0.00064      0.00245        0.178181818
0.001  0.7  0.49           0.00049      0.00294        0.213818182
0.001  0.6  0.36           0.00036      0.0033         0.24
0.001  0.5  0.25           0.00025      0.00355        0.258181818
0.001  0.4  0.16           0.00016      0.00371        0.269818182
0.001  0.3  0.09           0.00009      0.0038         0.276363636
0.001  0.2  0.04           0.00004      0.00384        0.279272727
0.991  0.1  0.01           0.00991      0.01375        1

                  P(o=2/2) 0.01375                

Теперь он говорит, что P (f> = 0,5) составляет 26%, по сравнению с предыдущим допущением 0,6%. Таким образом, Байес позволяет нам обновить нашу оценку вероятной стоимости I, Если объем данных невелик, он не говорит нам точно, какова стоимость, только то, что она достаточно велика, чтобы стоить исправления.

Еще один способ взглянуть на это называется Правило преемственности , Если вы переворачиваете монету 2 раза, и она поднимается вверх, оба раза, что это говорит о вероятном взвешивании монеты? Уважаемый ответ - сказать, что это бета-распределение со средним значением (количество ударов + 1) / (количество попыток + 2) = (2 + 1) / (2 + 2) = 75%.

(Ключ в том, что мы видим Iбольше чем единожды. Если мы увидим это только один раз, это не говорит нам много, f> 0.)

Таким образом, даже очень небольшое количество образцов может рассказать нам многое о стоимости инструкций, которые он видит. (И он будет видеть их с частотой, в среднем, пропорциональной их стоимости. nобразцы берутся и fэто стоимость, тогда Iпоявится на nf+/-sqrt(nf(1-f))образцы. Пример, n=10, f=0.3, то есть 3+/-1.4Образцы).


ADDED, чтобы дать интуитивное ощущение разницы между измерением и выборочной выборкой стека:
Теперь есть профайлеры, которые пробуют стек, даже на настенные часы, но что выходит это измерения (или горячая дорожка, или горячая точка, из которой «узкое место» может легко скрыться). То, что они не показывают вам (и они легко могли), это сами образцы. И если ваша цель - найти узкое место, количество которых вам нужно увидеть, в среднем , 2 делится на долю времени, которое требуется. Так что, если это займет 30% времени, в среднем отобразится 2 / .3 = 6.7 образцов, и вероятность того, что 20 образцов покажет, равна 99,2%.

Ниже приведена небольшая иллюстрация разницы между проверкой измерений и проверкой образцов стека. Узким местом может быть один большой капля, подобный этому, или многочисленные маленькие, это не имеет никакого значения.

enter image description here

Измерение производится горизонтально; он сообщает вам, какая часть времени требует определенных процедур. Выборка является вертикальной. Если есть какой-то способ избежать того, что делает вся программа в данный момент, и если вы видите это на втором образце , вы нашли узкое место. Вот что имеет значение - видя всю причину потраченного времени, а не только сколько.


1184



Вы можете использовать Valgrind со следующими параметрами

valgrind --tool=callgrind ./(Your binary)

Он будет генерировать файл, называемый callgrind.out.x, Затем вы можете использовать kcachegrindинструмент для чтения этого файла. Это даст вам графический анализ вещей с результатами, например, какие из них стоят.


463



Я предполагаю, что вы используете GCC. Стандартное решение было бы профилировать с помощью дргоЕ ,

Обязательно добавьте -pgдля компиляции перед профилированием:

cc -o myprog myprog.c utils.c -g -pg

Я еще не пробовал, но я слышал хорошие вещи о Google-perftools , Это определенно стоит попробовать.

Связанный вопрос Вот ,

Несколько других слов, если gprofне выполняет эту работу для вас: Valgrind , Intel VTune , Солнце DTrace ,


293



Newer kernels (e.g. the latest Ubuntu kernels) come with the new 'perf' tools (apt-get install linux-tools) AKA perf_events.

These come with classic sampling profilers (man-page) as well as the awesome timechart!

The important thing is that these tools can be system profiling and not just process profiling - they can show the interaction between threads, processes and the kernel and let you understand the scheduling and I/O dependencies between processes.

Alt text


212



I would use Valgrind and Callgrind as a base for my profiling tool suite. What is important to know is that Valgrind is basically a Virtual Machine:

(wikipedia) Valgrind is in essence a virtual machine using just-in-time (JIT) compilation techniques, including dynamic recompilation. Nothing from the original program ever gets run directly on the host processor. Instead, Valgrind first translates the program into a temporary, simpler form called Intermediate Representation (IR), which is a processor-neutral, SSA-based form. After the conversion, a tool (see below) is free to do whatever transformations it would like on the IR, before Valgrind translates the IR back into machine code and lets the host processor run it.

Callgrind is a profiler build upon that. Main benefit is that you don't have to run your aplication for hours to get reliable result. Even one second run is sufficient to get rock-solid, reliable results, because Callgrind is a non-probing profiler.

Another tool build upon Valgrind is Massif. I use it to profile heap memory usage. It works great. What it does is that it gives you snapshots of memory usage -- detailed information WHAT holds WHAT percentage of memory, and WHO had put it there. Such information is available at different points of time of application run.


63



This is a response to Nazgob's Gprof answer.

I've been using Gprof the last couple of days and have already found three significant limitations, one of which I've not seen documented anywhere else (yet):

  1. It doesn't work properly on multi-threaded code, unless you use a workaround

  2. The call graph gets confused by function pointers. Example: I have a function called multithread() which enables me to multi-thread a specified function over a specified array (both passed as arguments). Gprof however, views all calls to multithread() as equivalent for the purposes of computing time spent in children. Since some functions I pass to multithread() take much longer than others my call graphs are mostly useless. (To those wondering if threading is the issue here: no, multithread() can optionally, and did in this case, run everything sequentially on the calling thread only).

  3. It says here that "... the number-of-calls figures are derived by counting, not sampling. They are completely accurate...". Yet I find my call graph giving me 5345859132+784984078 as call stats to my most-called function, where the first number is supposed to be direct calls, and the second recursive calls (which are all from itself). Since this implied I had a bug, I put in long (64-bit) counters into the code and did the same run again. My counts: 5345859132 direct, and 78094395406 self-recursive calls. There are a lot of digits there, so I'll point out the recursive calls I measure are 78bn, versus 784m from Gprof: a factor of 100 different. Both runs were single threaded and unoptimised code, one compiled -g and the other -pg.

This was GNU Gprof (GNU Binutils for Debian) 2.18.0.20080103 running under 64-bit Debian Lenny, if that helps anyone.


49



The answer to run valgrind --tool=callgrind is not quite complete without some options. We usually do not want to profile 10 minutes of slow startup time under Valgrind and want to profile our program when it is doing some task.

So this is what I recommend. Run program first:

valgrind --tool=callgrind --dump-instr=yes -v --instr-atstart=no ./binary > tmp

Now when it works and we want to start profiling we should run in another window:

callgrind_control -i on

This turns profiling on. To turn it off and stop whole task we might use:

callgrind_control -k

Now we have some files named callgrind.out.* in current directory. To see profiling results use:

kcachegrind callgrind.out.*

I recommend in next window to click on "Self" column header, otherwise it shows that "main()" is most time consuming task. "Self" shows how much each function itself took time, not together with dependents.


47



Use Valgrind, callgrind and kcachegrind:

valgrind --tool=callgrind ./(Your binary)

generates callgrind.out.x. Read it using kcachegrind.

Use gprof (add -pg):

cc -o myprog myprog.c utils.c -g -pg 

(not so good for multi-threads, function pointers)

Use google-perftools:

Uses time sampling, reveals I/O and CPU bottlenecks are revealed.

Intel VTune is the best (free for educational purposes).

Others: AMD Codeanalyst, OProfile, 'perf' tools (apt-get install linux-tools)


8



These are the two methods I use for speeding up my code:

For CPU bound applications:

  1. Use a profiler in DEBUG mode to identify questionable parts of your code
  2. Then switch to RELEASE mode and comment out the questionable sections of your code (stub it with nothing) until you see changes in performance.

For I/O bound applications:

  1. Use a profiler in RELEASE mode to identify questionable parts of your code.

N.B.

If you don't have a profiler, use the poor man's profiler. Hit pause while debugging your application. Most developer suites will break into assembly with commented line numbers. You're statistically likely to land in a region that is eating most of your CPU cycles.

For CPU, the reason for profiling in DEBUG mode is because if your tried profiling in RELEASE mode, the compiler is going to reduce math, vectorize loops, and inline functions which tends to glob your code into an un-mappable mess when it's assembled. An un-mappable mess means your profiler will not be able to clearly identify what is taking so long because the assembly may not correspond to the source code under optimization. If you need the performance (e.g. timing sensitive) of RELEASE mode, disable debugger features as needed to keep a usable performance.

For I/O-bound, the profiler can still identify I/O operations in RELEASE mode because I/O operations are either externally linked to a shared library (most of the time) or in the worst case, will result in a sys-call interrupt vector (which is also easily identifiable by the profiler).


2



For single-threaded programs you can use igprof, The Ignominous Profiler: https://igprof.org/ .

It is a sampling profiler, along the lines of the... long... answer by Mike Dunlavey, which will gift wrap the results in a browsable call stack tree, annotated with the time or memory spent in each function, either cumulative or per-function.


2