Регистрация выполнения кода в C ++ - PullRequest
0 голосов
/ 14 апреля 2011

Много раз использовав gprof и callgrind , я пришел к (очевидному) выводу, что не могу эффективно использовать их при работе с большими (как в программе CAD, которая загружаетцелая машина) программы.Я думал, что, возможно, я мог бы использовать магию C / C ++ MACRO и каким-то образом создать простой (но приятный) механизм ведения журнала.Например, можно вызвать функцию с помощью следующего макроса:

#define CALL_FUN(fun_name, ...) \
    fun_name (__VA_ARGS__);

Мы могли бы добавить некоторые функции синхронизации / синхронизации до и после вызова функции, чтобы каждая функция, вызываемая с помощью CALL_FUN, получала время, например,

#define CALL_FUN(fun_name, ...) \
   time_t(&t0);                 \
   fun_name (__VA_ARGS__);      \
   time_t(&t1);

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

Итак, вот мой (первый) вопрос: Считаете ли вы этот подход подходящим?Если да, то как его можно улучшить, а если нет, то можете ли вы предложить лучший способ измерения времени и регистрации графов вызовов?

Коллега предложил другой подход к решению этой проблемы, который аннотируетсяконкретный комментарий к каждой функции (которую мы хотим зарегистрировать).Затем во время процесса make необходимо запустить специальный препроцессор, проанализировать каждый исходный файл, добавить логику регистрации для каждой функции, которую мы хотим зарегистрировать, создать новый исходный файл с вновь добавленным (синтаксическим анализом) кодом и вместо этого собрать этот код.Я предполагаю, что чтение макросов CALL_FUN ... (мое предложение) повсеместно - не лучший подход, и его подход решил бы эту проблему.Так что вы думаете об этом подходе?

PS: Я не очень хорошо разбираюсь в ловушках C / C ++ MACRO, поэтому, если это может быть разработано с использованием другого подхода, скажите, пожалуйста, так.

Спасибо.

Ответы [ 6 ]

2 голосов
/ 14 апреля 2011

Ну, вы могли бы использовать магию C ++ для встраивания объекта регистрации.что-то вроде

class CDebug 
{
CDebug() { ... log somehow ... }
~CDebug() { ... log somehow ... }

};

в ваших функциях, тогда вы просто пишете

void foo()
{
   CDebug dbg;
    ...

   you could add some debug info


   dbg.heythishappened()

   ...
}  // not dtor is called or if function is interrupted called from elsewhere.
1 голос
/ 05 октября 2012

Я немного опоздал, но вот что я делаю для этого:

В Windows есть переключатель компилятора / Gh , который заставляет компилятор вставлять скрытую функцию _penter в начале каждой функции. Существует также переключатель для получения _pexit вызова в конце каждой функции.

Вы можете использовать это для получения обратных вызовов при каждом вызове функции. Вот статья с более подробной информацией и примером исходного кода:

http://www.johnpanzer.com/aci_cuj/index.html

Я использую этот подход в своей пользовательской системе ведения журнала для хранения последних нескольких тысяч вызовов функций в кольцевом буфере. Это оказалось полезным для отладки при сбоях (в сочетании с MiniDumps).

Некоторые примечания по этому вопросу:

  • Влияние на производительность во многом зависит от вашего кода обратного вызова. Вы должны сделать это как можно проще.
  • Вам просто нужно сохранить адрес функции и базовый адрес модуля в файле журнала. Затем вы можете позже использовать SDK для доступа к интерфейсу отладки, чтобы получить имя функции из адреса (через файл PDB).

Все это работает на удивление хорошо для меня.

1 голос
/ 14 апреля 2011

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

Я могу понять, как работает матричный решатель 1Mx1M под valgrind, поэтому я бы предложил начать с так называемого «профилирования Монте-Карло».method "- запустить процесс и параллельно запустить pstack несколько раз, скажем, каждую секунду.В результате у вас будет N стековых дампов (N может быть весьма значительным).Тогда математическим подходом будет подсчитать относительные частоты каждого стека и сделать вывод о наиболее частых.На практике вы либо сразу видите узкое место, либо, если нет, переключаетесь на деление пополам, gprof и, наконец, на набор инструментов valgrind.

1 голос
/ 14 апреля 2011

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

В отличие от измерения скорости или получения информации о покрытии.

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

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

Вот еще объяснение.

0 голосов
/ 14 апреля 2011

Может быть, вы должны использовать профилировщик. AQTime является относительно хорошим вариантом для Visual Studio.(Если у вас VS2010 Ultimate, у вас уже есть профилировщик.)

0 голосов
/ 14 апреля 2011

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

class LOG {
    LOG(const char* ...) {
        // log time_t of the beginning of the call
    }
    ~LOG(const char* ...) {
        // calculate the total time spent,
        //by difference between current time and that saved in the constructor
    }
};

void somefunction() {
    LOG log(__FUNCTION__, __FILE__, ...);
    .. do other things
}

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

// ### LOG

и затем вы автоматически заменяете строку в отладочных сборках (не сложно).

...