Блог пользователя Avitella

Автор Avitella, 12 лет назад, По-русски

Введение

Идея поста появилась после обсуждения дженериков в java с Slamur. Он продемонстрировал на примере задачи 20C - Алгоритм Дейкстры? скорость работы кода с дженериками и без. Большая ли получилась разница, судите сами. Вот код с дженериками и без. Внятных объяснений разницы я загуглить не смог, поэтому тех, кто знает в чем дело, прошу разьяснить ситуацию. После этого я заинтересовался: а как обстоят дела с шаблонами C++? Да и не только с ними, но и вокруг них. Итак...

Поехали

Шаблоны C++ есть только в коде. В исполняемом файле их уже нет. Для каждого нужного типа создается свой класс/структура или функция. Тестировать различные способы я решил все на той же задаче про алгоритм Дейкстры(сначала). После измерил у себя искуственную задачу, но об этом позже. Изначальное желание просто протестировать реализацию с шаблонами и без вылилось в более широкий набор интересностей.

  • Реализация полностью на стандартных структурах данных
  • Своя шаблонная пара
  • Своя не шаблонная пара
  • Своя не шаблонная пара + свой не шаблонный контейнер
  • И наконец своя куча + свои пары, все не шаблонное

Стандартные структуры данных

Тут в общем-то рассказывать нечего. Просто протокол тестирования. Отмечу только время работы: 140 мс.

Свои шаблонные пары

Не раз видел комментарии, что std::pair — это тормозящая лажа. Я хоть и относился к ним с недоверием, но наивно полагал, что это не голословные утверждения. Точнее, полагал до тех пор, пока не заглянул в их исходный код. А вот и он. Да, выглядит страшно и непонятно, но если поразбираться, то становится совсем непонятно, почему же это они "тормозят". Однако я все равно решил написать свои шаблонные пары, и протестить их.

template<typename T, typename U>
struct Pair {
  T first;
  U second;
  
  Pair()
  { }
  
  Pair(T f, U s) :
    first(f),
    second(s)
  { }
  
  bool operator < (Pair<T, U> o) {
    return first < o.first 
        || (first == o.first && second < o.second);
  }
  
  bool operator > (Pair<T, U> o) {
    return first > o.first 
        || (first == o.first && second > o.second);
  }
  
  bool operator == (Pair<T, U> o) {
    return first == o.first && second == o.second;
  }
};

template<typename T, typename U>
Pair<T, U> make_Pair(T f, U s) {
  return Pair<T, U>(f, s);
}

Вот так они реализованы у меня. А теперь смотрим на протокол тестирования. Хм, те же 140 мс. Все получилось так же, как и со стандартными. Я не утверждаю, что стандартные плюсовые пары — идеал оптимизации, но они как минимум ничем не хуже своих "обычных" пар.

Уходим от шаблонов

Теперь давайте посмотрим, как поведут себя пары без шаблонов. Код прямо здесь приводить не буду, будет ссылка на посылку. Итог получился не сильно быстрее: 125 мс. Разница небольшая. Критично это или нет, наверное, стоит смотреть в индивидуальном порядке.

Финт ушами

Давайте вспомним, как объявляется очередь с приоритетами.
prioriry_queue< тип, контейнер, компаратор > С типом мы уже разобрались, это наши любимые не шаблонные пары. Компаратор тоже понятен. Давайте напишем свой контейнер на массиве, чтобы не происходили постоянные реаллокации. Для меня это было интересным опытом :) Сначала я написал то, что думаю, потом пытался скомпилить и исправлял вылезающие ошибки. И все-таки это удалось (:

struct cont {
  typedef Pair value_type;
  typedef Pair& reference;
  typedef const Pair& const_reference;
  typedef int size_type;
  
  int sz;
  Pair a[LIM];
  
  cont() :
    sz(0)
  { }
  
  value_type* begin() {
    return a;
  }
  
  value_type* end() {
    return a + sz;
  }
  
  void push_back(Pair x) {
    *(a + sz) = x;
    sz++;
  }
  
  void pop_back() {
    sz--;
  }
  
  const value_type front() {
    return *a;
  }
  
  size_type size() const  {
    return sz;
  }
  
  const_reference front() const  {
    return *a;
  }
  
  bool empty() const {
    return sz == 0;
  }
};

Результат получился интересный. 109 мс, но разница все равно небольшая...

Все мое!

Просто ссылка, результат не впечатлил. 156 мс. Не сильно, но дольше.

Поехали, дубль 2

Хорошая, конечно, задача, но там есть и еще всякие факторы, влияющие на время, да и данных не так много, да и тесты не идеальные. И вот, что сделал: создал файл, на достаточно большое количество данных и протестировал все на нем. Создавал так:

fout = open("in", "w")

arr = [100, 1000, 10000]

for m in arr:
    for i in xrange(1, m + 1):
        for j in xrange(1, m + 1):
            fout.write(str(i) + " " + str(j) + '\n')

Компилировал плюсовые коды так:

g++ -O2 -Wall -o "a" "a.cpp"

И вот, что получилось:

1) Только STL

2) Мои шаблонные пары

3) Мои не шаблонные пары

4) Мой контейнер

5) Моя очередь с приоритетами

Выводы

Ну что тут сказать... Мой код не претендует на премию за чудеса оптимизации, но он и не самый худший из того, что можно было бы написать. На мой взгляд разница совсем не существенная, что показало "тяжелое" тестирование. Особенно если принять во внимание, что на результаты влиял не только код, но и общее поведение системы(да и хром работал). Пожалуй, сказать можно только то, что моя реализация кучи проигрывает стандартной :) А остальное практически идентично отработало. Итого: STL — добро! Критика приветствуется. Все еще помните вопрос про дженерики в java? Очень хочется узнать ответ.

  • Проголосовать: нравится
  • +30
  • Проголосовать: не нравится

»
12 лет назад, # |
Rev. 2   Проголосовать: нравится +11 Проголосовать: не нравится

Ты пробовал в Java-коде использовать в классах, которые вместо "генериков" Integer и Long, а не int, long?

UPD: я к тому, что я не понимаю, как могут тормозить генерики. А вот в автобоксинге дело вполне может быть

  • »
    »
    12 лет назад, # ^ |
    Rev. 4   Проголосовать: нравится 0 Проголосовать: не нравится

    Если я правильно понял вопрос, то нет. Ты имеешь в виду, что при объявлении переменной в класса писать не так int x; long y, а так Integer x; Long y? Нет, так я не пробовал. Сейчас посмотрю, что получится.

    UPD. Блин, она теперь падает, а я не так хорошо шарю в джаве, чтобы на вскидку узнать из-за чего. Есть какие-нибудь особенности при замене int на Integer и long на Long?

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Что говорит?

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится
        java.lang.ClassCastException: Main$Vertex cannot be cast to java.lang.Comparable
        	at java.util.PriorityQueue.siftUpComparable(PriorityQueue.java:633)
        	at java.util.PriorityQueue.siftUp(PriorityQueue.java:629)
        	at java.util.PriorityQueue.offer(PriorityQueue.java:329)
        	at java.util.PriorityQueue.add(PriorityQueue.java:306)
        	at Main.dijkstra(Main.java:415)
        	at Main.solve(Main.java:364)
        	at Main.run(Main.java:159)
        	at java.lang.Thread.run(Thread.java:722)
        
»
12 лет назад, # |
Rev. 2   Проголосовать: нравится +6 Проголосовать: не нравится

Шаблон класса в С++ всего лишь шаблон (звиняюсь за тафтологию :) ), а не окончательный класс. Когда мы в него подставляем аргументы, компилятор по нему генерирует код функции/класса и использует ее. Поэтому разницы никакой нет. В вашем случае возникла небольшая разница наверняка из-за того, что вы не кидаете исключения, не используете аллокаторы и прочее, что требует Стандарт от реализации STL.

»
12 лет назад, # |
Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

BTW, Помнится именно priority_queue в одном из {MSVC, g++}, здесь на КФ была по скорости, как рукописная, а в другом изрядно тормозила.

»
12 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Стоп, или я туплю, или cont не куча? Что-то не вижу я, где поддерживается свойств кучи. Похоже на обычный вектор/стек.

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится +10 Проголосовать: не нравится

    cont — контейнер, который я передаю в priority_queue. Стандартная очередь с приоритетами использует вектор, как контейнер, по дефолту. Я заменил вектор на структуру с массивом, чтобы не было реаллокаций. То есть cont — аналог vector, но не динамический.

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Спасибо, не знал/ не помнил о такой возможности.

»
12 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Когда-то тоже поднимался вопрос об STL, но чуток другой. В комментариях я описал, как пользоваться вектором, чтобы не тормозило. Но тогда к этому отнеслись с недоверием (((. Может следует сделать аналогичное сравнение вектора с массивом на какой-нибудь задаче?

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

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

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится +12 Проголосовать: не нравится
      template<typename T>
      struct my_vector : public vector<T>
      {
          static const int lim = 100005;
      
          explicit my_vector ()
              : vector<T>()
          {
              vector<T>::reserve(lim);
          }
      };
      
      priority_queue<int, my_vector<int> > q;
»
12 лет назад, # |
Rev. 3   Проголосовать: нравится +7 Проголосовать: не нравится

Два приведенных примера на джаве достаточно капитанские: в одном случае полем класса является примитивный тип, а в другом — объект.

Вообще, любые вот эти параметры типов T — они же на самом деле просто как Object хранятся, а перед тем, как вернуть значение, кастуются к необходимому типу. Посмотрите хотя бы ArrayList.java, как это все устроено.

»
12 лет назад, # |
  Проголосовать: нравится +5 Проголосовать: не нравится

По поводу дженериков в C++: попробуйте взять любую задачу на FFT и поменять в ней complex<double> на собственную структуру, скорость возрастет в 2-2.5 раза. Хотя я пока склонен винить кривую реализацию complex. Интересно было бы узнать, что на самом деле.

  • »
    »
    12 лет назад, # ^ |
      Проголосовать: нравится -7 Проголосовать: не нравится

    Плюсовые шаблоны раскрываются на этапе компиляции, и во время выполнения вообще ни на что не влияют. Разве что, с ними сложнее работать оптимизатору.

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

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

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится 0 Проголосовать: не нравится

        GCC:

        private:
          _Tp _M_real;
          _Tp _M_imag;
        • »
          »
          »
          »
          »
          12 лет назад, # ^ |
            Проголосовать: нравится 0 Проголосовать: не нравится

          Да, я в студии смотрел. Но задачу сдавал на g++. Может, потому что inline не везде написано?

          • »
            »
            »
            »
            »
            »
            12 лет назад, # ^ |
              Проголосовать: нравится +7 Проголосовать: не нравится

            Было бы неплохо из решения задачи выделить тест-кейс, который показывает, что std::complex медленнее самописной структуры. Тогда можно было бы подебаггить.

    • »
      »
      »
      12 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      А никто и не говорит, что виноваты шаблоны. И оптимизатору без разницы, что там шаблоны.

      Но вот например, complex в реализации gcc не обращается к полям, а вызывает функции real и imag, которые объявлены без inline. Например, так делает operator +=. И конструктор копирования, который вообще непонятно зачем написали. Кстати, operator += тоже без inline. operator + реализован с inline, но через присваивание. Скушает ли все это оптимизатор?

      • »
        »
        »
        »
        12 лет назад, # ^ |
          Проголосовать: нравится +10 Проголосовать: не нравится

        Если функция определена внутри самого определения класса, то она по умолчанию inline.

        Можно проверить, что скушает: http://pastie.org/5667770

»
12 лет назад, # |
  Проголосовать: нравится +11 Проголосовать: не нравится

В STL есть известные больные места. Например, std::string тормознутый, std::list неудобный, std::stack и std::queue зачастую реализованы через std::deque, а константа у std::deque несколько хуже того же std::vector. Деструкторы в дебаге могут проверять память и тормозить, приходится применять извращения. В любом случае, пока не запихивается константа, STL резко сокращает код. Этим сказано все.