Подтвердить что ты не робот

Сортировка векторов строк: plain C vs idiomatic С++ 11

В настоящее время я пытаюсь изучить С++ 11 и его причудливые функции. Чтобы быть конкретным, я ищу высокую эффективность. Поэтому я с радостью написал программу на С++ 11 для сортировки строк входного файла для проверки моих новых навыков. Из-за встроенных и приятных функций компиляторов С++ я ожидал высокой производительности на этом небольшом примере. Чтобы понять, насколько быстро была моя программа, я взломал точно такую ​​же программу на C, используя функцию qsort, так как она является исходной C. Для этой функции не выполняется инкрустация, и моя функция сравнения вызывается с косвенностью и должна выполняться две ссылки для доступа к указателям char *, представляющим строки.

Факты

Тем не менее, я был очень удивлен результатами, C кажется в 4 раза быстрее, чем С++. В файле 8 Мб я получаю следующие результаты:

$ g++ -O3 -std=c++11 -o sort sort.C
$ time ./sort < huge > /dev/null

real    0m0.415s
user    0m0.397s
sys     0m0.013s

$ cc -O3 -Wall -o sortc sort.c
$ time ./sortc < huge  > /dev/null

real    0m0.104s
user    0m0.097s
sys     0m0.010s

$ wc -l huge
140190 huge

Обратите внимание, что я старался быть как можно более честным, параметры компиляции одинаковы, а моя программа на C (сбрасываемая позже) ведет себя так же, как и С++: нет ограничений на размер входных строк и нет ограничений на количество строк ввода.

Я также заметил, что, хотя моя программа C вызывает malloc почти один раз для каждой строки ввода, программа С++ имеет отношение 10 распределений на строку ввода!

Код

Вот две программы, которые я использовал для сравнения.

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <memory>

int main () {
    typedef std::vector<std::string> svec;
    svec a;
    std::string s;

    for (;;) {
        getline(std::cin, s);
        if (std::cin.eof()) {
            if (s != "")
                a.push_back(std::move(s));
            break;
        }
        a.push_back(std::move(s));
    }
    std::sort(a.begin(), a.end());
    for (std::string &s : a) {
        std::cout << s << "\n";
    }
}

И моя гораздо более подробная версия C.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define BUFSZ 100
size_t getl(char **line, size_t len) {
        char buf[BUFSZ];
        size_t i, n;

        for (i=0; i<BUFSZ; i++) {
                int c = getchar();

                if (c == EOF || c == '\n') {
                        *line = malloc(len+i+1);
                        memcpy(&(*line)[len], buf, i);
                        (*line)[len+i] = 0;
                        return i;
                }
                buf[i] = c;
        }

        n = getl(line, len+i);
        memcpy(&(*line)[len], buf, i);
        return i+n;
}

#define ARRAYSZ 30
struct Array {
        char **lv;
        size_t li, lc;
};

void addline(struct Array *a, char *line) {
        if (a->li == a->lc) {
                a->lc *= 2;
                a->lv = realloc(a->lv, a->lc * sizeof *a->lv);
        }
        a->lv[a->li++] = line;
}

int cmp(const void *a, const void *b) {
        return strcmp(*(const char **)a, *(const char **)b);
}

int main(void) {
        char *line;
        struct Array a;
        size_t i;

        a.li = 0;
        a.lc = ARRAYSZ;
        a.lv = malloc(a.lc * sizeof *a.lv);

        for (;;) {
                getl(&line, 0);
                if (feof(stdin)) {
                        if (line[0] != 0)
                                addline(&a, line);
                        else
                                free(line);
                        break;
                }
                addline(&a, line);
        }
        qsort(a.lv, a.li, sizeof *a.lv, cmp);
        for (i=0; i<a.li; i++) {
                printf("%s\n", a.lv[i]);
                free(a.lv[i]);
        }
        free(a.lv);
        return 0;
}

Вопрос

Может ли кто-нибудь сказать мне, где моя программа на С++ должна быть изменена (не становясь простой C) быстрее? Я старался оставаться очень идиоматичным, это хороший способ взломать С++ или я должен писать код C-like, когда хочу высокой производительности? Почему программа С++ выделяет столько в куче, как я могу уменьшить это?

редактирует

По многочисленным просьбам я показываю результаты профилирования моей программы на С++. Вот забавный вывод профилировщика моей программы на С++ (первые две строки):

Each sample counts as 0.01 seconds.
 %   cumulative   self              self     total           
time   seconds   seconds    calls  ms/call  ms/call  name    
40.03      0.02     0.02  1198484     0.00     0.00  __gnu_cxx::__normal_iterator<std::string*, std::vector<std::string, std::allocator<std::string> > >::operator--()
30.02      0.04     0.02  2206802     0.00     0.00  bool std::operator< <char, std::char_traits<char>, std::allocator<char> >(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)

Как я читал, кажется, что выделение не является единственной причиной.

4b9b3361

Ответ 1

Причиной является синхронизация С++ std io. Следующий код:

int main () {
    typedef std::vector<std::string> svec;
    svec a;
    std::string s;

    // note
    std::ios_base::sync_with_stdio(false);

    for (;;) {
    getline(std::cin, s);
    if (std::cin.eof()) {
        if (s != "")
            a.push_back(std::move(s));
        break;
    }
        a.push_back(std::move(s));
    }
    std::sort(a.begin(), a.end());
    for (std::string &s : a) {
        std::cout << s << "\n";
    }
}

дает

 real   0m0.106s
 user   0m0.104s
 sys    0m0.004s

C-версия дает следующее:

 real   0m0.167s
 user   0m0.164s
 sys    0m0.000s

Изменить: поскольку RiaD правильно упомянул sync_with_stdio, конечно, является статической функцией, поэтому достаточно вызвать функцию один раз для всех потоков std io.

Ответ 2

Вы также используете две разные библиотеки ввода-вывода. Это полностью испортит любую информацию о времени, так как библиотеки ввода/вывода C и С++ сильно отличаются друг от друга. Потоки IO просты, не рассчитаны на скорость.

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

Вам нужно чисто время, затраченное на сортировку ранее существовавшего std::vector<std::string>, скажем.

О да, и ваш getl полон утечки памяти.

Ответ 3

Я предполагаю, что вы не измеряете скорость сортировки, а перераспределяете память. Вместо того, чтобы делать a.push_back по одному элементу за раз, попробуйте выделить фронт векторной памяти так же, как в программе C

a.reserve(num_lines);

В зависимости от того, использует ли ваш компилятор перераспределение с коэффициентом расширения 1.5 (VС++) или 2 (g++), у вас есть 29 и 17 перераспределение с 140,190 линиями в вашем файла (т.е. log(total lines) / log(expansion factor)).

Комментарий R. Martinho Fernandes также попадает в гвоздь: используйте std::chrono::high_resolution_clock::now() вокруг операторов sort в обеих программах, чтобы получить разницу во времени. Это изолирует вас от различий в памяти и IO.

Ответ 4

Другие отметили, что большая часть того, что вы измеряете, это скорость библиотеки ввода-вывода. Я думаю, что стоит отметить, однако, что в отличие от некоторых заявлений, которые были сделаны здесь, С++ iostreams могут быть полностью конкурентными с I/O, используя C FILE * s. Итак, что вы измеряете, в основном "как дерьмовые gcc iostreams?", А не "как дерьмовые являются iostreams в целом?"

Например, я начал с объединения всех файлов .txt, которые у меня были в одном каталоге, чтобы создать довольно большой текстовый файл. Затем я скомпилировал ваш код C с помощью VС++ 10 и использовал его для сортировки этого файла (запись вывода в другой файл). Это продолжалось через 3,2 секунды.

Я также написал то, что я считаю разумным идиоматическим С++, чтобы выполнить ту же задачу:

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <iterator>

class line { 
    std::string data;
public:
    friend std::istream &operator>>(std::istream &is, line &l) { 
        return std::getline(is, l.data);
    }
    operator std::string() const { return data; }
};

int main() { 
    std::vector<std::string> vec(
        (std::istream_iterator<line>(std::cin)),
        std::istream_iterator<line>());

    std::sort(vec.begin(), vec.end());
    std::copy(vec.begin(), vec.end(), 
        std::ostream_iterator<std::string>(std::cout, "\n"));

    return 0;
}

Это примерно такая же длина, как ваш код на С++, но (я бы требовал) проще, чем любая из ваших программ. Если бы вы действительно заботились, было бы довольно легко сделать его еще короче. Это не делает никакой конкретной попытки оптимизации вообще. Скомпилированный с VС++ 10 (с теми же самыми флажками оптимизации, что и на вашем C-коде - -O2b2 -GL), он работает в 2,8 секунды, что примерно на 10% быстрее, чем ваш код C.

Я бы ожидал, что если вы запустите это в Linux, вы обнаружите, что он медленнее, чем ваш C-код. Добавление двух вызовов sync_with_stdio(false);, вероятно, исправит это, как и в случае с вашим кодом на С++. Обычно вызовы sync_with_stdio(false); в значительной степени влияют на Linux, но я никогда не мог измерить какие-либо улучшения от их использования в Windows (с любым из компиляторов, которые я пробовал - недавно VС++ и MinGW, и еще давно Intel, Comeau, CygWin и Borland).

Ответ 5

Проблемы ввода-вывода в стороне (не говоря уже о том, что они не являются подлинными), эти два вида делают две разные вещи. Версия С++ перемещает объекты string вокруг, а версия C перемещает указатели. Последний, как правило, (см. Ниже) будет быстрее. Перепишите код С++ для перемещения указателей вместо объектов, т.е. Используйте std::vector<std::string*> и явно новый каждый объект string. Да, это не идиоматично, но это более справедливое сравнение скорости.

Если std::sort перемещается, движущиеся строки становятся намного быстрее, а std::vector<std::string> не страдает нигде почти так же. Но семантика переноса является новой в С++ 11, и даже если у вас есть компилятор С++ 11, перемещение семантики, возможно, не было перенесено в std::sort.