Простой профилировщик времени выполнения для исполняемых файлов

Я уверен, что здесь уже есть несколько профилировщиков, написанных на C ++, хотя у этого цели совсем другие. Я попытался сделать его как можно более надежным или «защищенным от дурака», если хотите, чтобы было практически невозможно сбой из-за необработанного исключения или неопределенного поведения.

Он также поддерживает перенаправление stdin, stdout и stderr в файлы.

#include <boost/process.hpp>
#include <boost/chrono.hpp>
#include <exception>
#include <fstream>
#include <string>
#include <vector>
#include <iostream>
#include <memory>
#include <unordered_map>
#include <stdexcept>
#include <cstdio>

typedef boost::chrono::steady_clock bench_clock;

// Defined map of acceptable arguments to integer for switch
const static std::unordered_map<std::string, int> string_map{
    {"--count", 1},
    {"--in", 2},
    {"--out", 2},
    {"--err", 2},
    {"process", 3}
};

// Return code of argument, or 0 if it's not defined
int string_case(std::string& s_case)
{
    return string_map.count(s_case) ? string_map.at(s_case) : 0;
}

// Check if string is numeric
bool is_digits(const std::string& str)
{
    return str.find_first_not_of("0123456789") == std::string::npos;
}

int main(int argc, char* argv[])
{
    namespace bp = boost::process;

    if(argc < 2)
    {
        std::cout << "No runnable executable specified.n";
        return EXIT_FAILURE;
    }

    std::vector<std::string> arguments(argv + 1, argv + argc);
    auto it = std::begin(arguments);

    const auto executable_path = *it;

    std::vector<std::string> proc_arguments;
    unsigned long long count = 1;
    std::string string_num;
    std::string in_path; // Default initialized to ""
    std::string out_path;
    std::string err_path;
    
    for(const auto end = std::end(arguments); it+1 != end;)
    {
        switch(string_case(*(++it)))
        {
            default:
                std::cout << "Unexpected or illegal argument encountered: " << *it << "n";
                return EXIT_FAILURE;
            
            case 1:
                if (it + 1 == end)
                {
                    std::cout << "Unexpected end of argument list.n";
                    return EXIT_FAILURE;
                }

                string_num = *(++it);
            
                if(!is_digits(string_num))
                {
                    std::cout << "Argument for --count is either negative or contains non-numeric characters.n";
                    return EXIT_FAILURE;
                }
            
                try {
                    count = std::stoull(string_num);
                } catch(std::out_of_range&)
                {
                    std::cout << "Number too large for --count.n";
                    return EXIT_FAILURE;
                } catch (std::invalid_argument&)
                {
                    std::cout << "--count can only accept numeric values.n";
                    return EXIT_FAILURE;
                }

                if(count == 0)
                {
                    std::cout << "Count must be larger than 0.n";
                    return EXIT_FAILURE;
                }
            
                break;
            
            case 2:
                if (it + 1 == end)
                {
                    std::cout << "Unexpected end of argument list.n";
                    return EXIT_FAILURE;
                }

                if (*it == "--in")
                    in_path = *(++it);
                else if (*it == "--out")
                    out_path = *(++it);
                else if (*it == "--err")
                    err_path = *(++it);

                break;

            case 3:
                while(it + 1 != end)
                {
                    proc_arguments.push_back(*(++it));
                }
            
                break;
        }
    }

    auto out_stream = out_path.empty() ? (bp::std_out > stdout) : (bp::std_out > out_path);
    auto err_stream = out_path.empty() ? (bp::std_err > stderr) : (bp::std_err > err_path);

    if (!out_path.empty()) {
        std::ofstream ofs_out(out_path);
        if (!ofs_out.is_open())
        {
            std::cout << "STD_OUT file cannot be opened for writing.n";
            return EXIT_FAILURE;
        }
        ofs_out.close();
    }

    if (!err_path.empty()) {
        std::ofstream ofs_err(err_path);
        if (!ofs_err.is_open())
        {
            std::cout << "STD_ERR file cannot be opened for writing.n";
            return EXIT_FAILURE;
        }
        ofs_err.close();
    }

    if (!in_path.empty()) {
        std::ifstream ifs_in(in_path);
        if (!ifs_in.is_open())
        {
            std::cout << "STD_IN file cannot be opened for writing.n";
            return EXIT_FAILURE;
        }
        ifs_in.close();
    }

    auto first = true;
    long double duration = 0;
    long double n = 1;
    int exit_code = 0;
    
    for (unsigned long long iter = 0; iter < count; iter++)
    {
        if (!in_path.empty() && !out_path.empty() && !err_path.empty() && count > 1) {
            std::string s(7, '');
            auto str_out = std::snprintf(&s[0], s.size(), "%.2f", (static_cast<double>(iter + 1) / static_cast<double>(count)) * 100);
            s.resize(str_out);
            std::cout << "Progress: " << iter + 1 << "/" << count << " ... " << s << "% done." << 'r' << std::flush;
        }
        
        std::unique_ptr<bp::child> proc;
        boost::chrono::time_point<boost::chrono::steady_clock> start;
        boost::chrono::time_point<boost::chrono::steady_clock> end;

        if(in_path.empty())
        {
            std::cout << "Child process may be awaiting input from stdin:n";
        }
        
        try 
        {
            if (in_path.empty()) {
                start = bench_clock::now();
                proc = std::make_unique<bp::child>(bp::exe(executable_path), bp::std_in < stdin, out_stream, err_stream, bp::args(proc_arguments));
            }
            else {
                start = bench_clock::now();
                proc = std::make_unique<bp::child>(bp::exe(executable_path), bp::std_in < in_path, out_stream, err_stream, bp::args(proc_arguments));
            }
            (*proc).wait();
            end = bench_clock::now();
            exit_code = proc->exit_code();
            
        }
        catch (std::exception const& e)
        {
            std::cout << e.what();
            return EXIT_FAILURE;
        }

        if(count == 1)
        {
            std::cout << "Program completed with exit code " << exit_code << "n";
        }

        if (iter == 1 && count >= 5)
            continue;

        n++;

        auto interval = static_cast<long double>(boost::chrono::duration_cast<boost::chrono::microseconds>(end - start).count());
        if (first) {
            duration += interval;
            first = false;
        }
        else
            duration = (duration * (n - 1) + interval) / n;
    }

    if(exit_code != 0)
    {
        std::cout << "Warning, program may have crashed or thrown an exception mid run, profiling may be inaccurate.n";
    }

    if(duration <= 0)
        std::cout << "nMeasured duration: " << duration*1000 << " nanosecondsn";
    else if(duration <= 1000)
        std::cout << "nMeasured duration: " << duration << " microsecondsn";
    else if(duration <= 1000000)
        std::cout << "nMeasured duration: " << duration / 1000 << " millisecondsn";
    else
        std::cout << "nMeasured duration: " << duration / 1000000 << " secondsn";
    
    return EXIT_SUCCESS;
}

Будем рады любым отзывам о том, что можно улучшить! Readme с инструкциями по запуску доступно на моей странице github для кода

1 ответ
1

Это больше похоже на «таймер», чем на «профилировщик». (Он не предоставляет информации о том, что на самом деле занимает время внутри дочернего процесса).


Структура программы:

  • Мы могли бы сделать программу более читаемой, убрав почти весь код из main. У нас должны быть отдельные функции для анализа аргументов командной строки и определения времени. например:

     int main(int argc, char** argv)
     {
         auto options = parse_args(argc, argv); // returns std::optional<program_options>
    
         if (!options)
             return EXIT_FAILURE;
    
         run_process(options.value());
     }
    
  • Есть и другие места, где код должен быть разделен на отдельные функции, например, парсинг беззнакового int из строки, вывод хода выполнения, запуск дочернего процесса.


Разбор аргументов:

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

  • Я не думаю, что есть много пользы от поиска строк аргументов на карте, перевода строки в int, а затем используя это int в операторе переключения.

    Мы можем сформулировать аргумент std::string (или же std::string_view), а затем сделайте if (arg == "--count") { ... } else if (arg == "--in") { ... } ... вместо.

  • std::stoull будет разбирать частичные строки на целые числа без знака. Вероятно, мы должны убедиться, что использовалась вся строка, чтобы избежать принятия чего-то вроде «123. ~ sdfkshjidf» как «123». Мы можем сделать это, проверив второй аргумент (pos) указывает конец строки после вызова функции.

    С этой проверкой нам, вероятно, не понадобится is_digits уточняйте заранее.

  • Делать *(++it) и сравнивая it + 1 == end возможно немного необычно. Более распространенная идиома — проверить it == end перед чтением, а затем сделайте *it++ для чтения и увеличения итератора.

Мы можем немного прояснить смысл кода, анализируя отдельные типы в отдельных функциях, например parse_string или parse_ull. Я бы, наверное, выбрал что-то вроде:

struct program_options
{
    std::string m_exe_path;
    std::uint64_t m_count = 1;
    std::string m_in_path, m_out_path, m_err_path;
    std::vector<std::string> m_process_args;
};

std::optional<program_options> parse_args(int argc, char** argv)
{
    auto print_usage = [&] ()
    {
        std::cout << "usage: ...n";
    };
    
    auto parse_string = [&] (char**& arg, char** end, char const* error_str) -> std::optional<std::string>
    {
        if (arg == end)
        {
            std::cout << error_str << "not enough arguments.n";
            print_usage();
            return { };
        }

        return *arg++;
    };

    auto parse_ull = [&] (char**& arg, char** end, char const* error_str) -> std::optional<std::size_t>
    {
        if (arg == end)
        {
            std::cout << error_str << "not enough arguments.n";
            print_usage();
            return { };
        }

        try
        {
            auto str = std::string(*arg++);
            auto pos = std::size_t{ 0 };
            auto value = std::stoull(str, &pos);

            if (pos != str.size())
            {
                std::cout << error_str << "expected an integer.n";
                print_usage();
                return { };
            }

            return value;
        }
        catch (std::invalid_argument&)
        {
            std::cout << error_str << "expected an integer.n";
            print_usage();
            return { };
        }
        catch (std::out_of_range&)
        {
            std::cout << error_str << "value out of valid range.n";
            print_usage();
            return { };
        }
    };

    auto arg = argv + 1; // argv[0] is the current program name
    auto const end = argv + argc;

    auto options = program_options();

    auto exe_path = parse_string(arg, end, "Failed to read executable path: ");
    if (!exe_path) return { };
    options.m_exe_path = exe_path.value();

    for ( ; arg != end; )
    {
        auto arg_sv = std::string_view(*arg++);

        if (arg_sv == "--count")
        {
            auto count = parse_ull(arg, end, "Failed to read --count: ");
            if (!count) return { };
            options.m_count = count.value();
        }
        else if (arg_sv == "--in")
        {
            auto in_path = parse_string(arg, end, "Failed to read --in: ");
            if (!in_path) return { };
            options.m_in_path = in_path.value();
        }
        else if (arg_sv == "--out")
        {
            auto out_path = parse_string(arg, end, "Failed to read --out: ");
            if (!out_path) return { };
            options.m_out_path = out_path.value();
        }
        else if (arg_sv == "--err")
        {
            auto err_path = parse_string(arg, end, "Failed to read --err: ");
            if (!err_path) return { };
            options.m_err_path = err_path.value();
        }
        else if (arg_sv == "process")
        {
            options.m_process_args.assign(arg, end);
            arg = end;
        }
    }

    return options;
}

(C ++ всегда немного неудобен для подобных вещей. Нам нужно либо использовать обработку исключений для управления потоком (хотя странный ввод пользователя никогда не бывает действительно «исключительным»), либо логическое возвращаемое значение (или код ошибки) в паре со ссылочным выходным аргументом (вроде как старомодно), или std::optional (досадно многословно).)


Я не думаю, что нужно много открывать, а затем закрывать файлы ввода / вывода / ошибок заранее. Даже если здесь он преуспеет, он все равно может потерпеть неудачу позже.

  • Спасибо за отличный отзыв! Я обязательно учусь на этом в будущем. В strtoull пришлось разбирать с is_digit() потому что он может преобразовывать отрицательные числа, что является нежелательным поведением, без создания исключения. Поэтому вместо добавления отдельной проверки на «-» в начале я просто проанализировал все это целиком. Что касается открытия и закрытия файлов ввода / вывода … сам процесс boost не сообщает о каких-либо проблемах при попытке открыть их, даже если ti терпит неудачу, поэтому хорошо иметь хотя бы небольшую проверку, чтобы увидеть, может ли файл по крайней мере, я думаю

    — Джек Аванте

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *