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

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

Сегодня столкнулся с непонятным для меня проседанием по времени на GCC решения, которое нормально заходит под MSC++.

Есть код, в котором что-то примерно такое:

struct point{double x,y;};

vector<pair<point,long > > v;

bool cmp(pair<point, int> a,pair<point, int> b)
{
     if (a.first.x>b.first.x)return true;
     if (a.first.x<b.first.x)return false;
     if (a.first.y>b.first.y)return true;
     return false;
}

int main()
{
sort(v.begin(),v.end(),cmp);
}

Тестирование в "запуске" показало, что при отправке решения под GCC время исполнения превышает время исполнения под студией в 9-12 раз.

Если исправить типы pair<point,long > / pair<point, int> (чтобы пары по описанию точно совпадали в векторе и в сортировке, не важно, будет в обеих случаях int или long), то это ускоряет программу процентов на 30, но все равно время выполнения больше студийного в 6-8 раз.

Кто-то может объяснить мне причины такой ощутимой разницы?

UPD. Если переписать просто на вектор точек (заменить пары на сами точки), то все равно разница сохраняется.

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

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

Изменится ли что-то, если сделать

inline bool cmp(const pair<point, long>& a, const pair<point, long>& b)

?

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

или так еще попробуй

struct C {
    inline bool operator ()(const T& a, const T& b) const {
        //
    }
};

sort(v.begin(), v.end(), C());

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

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

    Кстати по поводу inline, есть пример, в котором добавление этого слова реально даёт выигрыш при включенном O2?

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

      в рекурсиях нехило часто помогает

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

        Я не спорю с этим утверждением, но если это действительно так, то не мог бы кто-нибудь объяснить почему?

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

          Ну видимо код инлайнится с некоторой глубиной, что позволяет сократить глубину рекурсии(читать кол-во вызовов функций) в эти несколько раз

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

      У меня есть такой пример. Некоторое время назад решали Московский четвертьфинал 2009, и там была задача I, решающаяся dfs-ом огромной глубины (хотя, я чувствую, существует нормальное решение). Отправили, получили ML (он был 128 МБ), дописали inline к dfs, отправили, получили AC. Потом посмотрели: решение работало примерно в 2 раза быстрее и кушало примерно на 30 метров меньше памяти.

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

Если просто заинлайнить — никаких изменений.

Если изменить способ передачи в функцию, то очень заметное ускорение. Но все равно GCC далеко позади.

Для 500000 точек время выполнения было 0.45/3.5, стало 0.25/1.

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

    Ну хорошо. Тогда предыдущие изменения + перед cmp добавить

    #pragma GCC optimize ("fast-math")

    Что теперь?

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

      Без изменений.

      Если есть желание что-то пробовать, менять и проверять — отослал полный исходник.

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

      Вы не могли бы дать какую-нибудь ссыль на материал о #pragma GCC optimize. Меня интересует, какого рода optimize можно делать.) И вообще, есть ли смысл делать их вручную?

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

        Это всего лишь механизм, как задать дополнительные -O или -f параметры, если нельзя поменять строку запуска компилятора. То есть, на своём компьютере вместо этой строчки можно было бы просто дописать -ffast-math к параметрам компилятора.

        http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html#index-g_t_0040code_007boptimize_007d-function-attribute-2663

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

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

Возможно, Visual Studio с оптимизациями по умолчанию догадывается, что копирование point (1) не имеет побочных эффектов и (2) не требуется. И поэтому заменил объявление

bool cmp (pair <point, int> a, pair <point, int> b)

на

bool cmp (pair <point, int> const & a, pair <point, int> const & b)

У меня с mingw-g++ 4.7.2 это локально дало ускорение в несколько раз.

А по-хорошему, учитывая (1) и (2), можно заинлайнить всю функцию. Но у меня g++ наотрез отказывается это делать, даже с

inline bool cmp (pair <point, int> const & a, pair <point, int> const & b) __attribute__ ((always_inline));

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

UPD: исходник тут.

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

    Вот исходник после всех названных выше оптимизаций.

    Теперь я не могу понять, чем же мой код принципиально отличается. Потому как в примере выше — время выполнения примерно одинаковое.

    А мой код под студией все равно работает в 3.5 раза быстрее.

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

    Почему не inline bool cmp(const pair <point, int>& a, const pair <point, int>& b)? И почему это эквивалентное объявление дает ускорение небольшое ?:/

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

      На всякий случай замечу, что const T действительно должно быть эквивалентно T const. Запись T const (исправил) понятнее: если слева от const есть тип, то величину этого типа запрещается менять. Например, int const * const * x означает, что нельзя менять ни int, ни int *, а int * * менять можно. Ну а если слева от const ничего нет, ему приходится взять следующий тип справа.

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

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

        Видимо, так оно и есть. У меня теперь тоже нет разницы.

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

Кажется, вот еще одно ускорение : stable_sort вместо sort. Видимо, дело в специфике данных.

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

    Можно как-то более конкретно — какая здесь специфика, и в чем отличия между двумя семействами компиляторов в данных условиях? Был бы рад понять это на идейном уровне, ведь не хотелось бы наступать на эти грабли опять, как только условия немножко изменятся:)

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

      Ну, утверждать не берусь, смотреть лень, но, видимо, в MSC++ всегда используется merge_sort, а в g++ sort() начинается с quick_sort, в то время как stable_sort также реализован на merge_sort (про g++ уверен на 95%). В случае, если изначально данные не совершенно случайные — есть частичная отсортированность, много равных элементов, стоимость swap-а очень высока — stable_sort работает быстрее, на случайных же данных quick_sort все-таки быстрее. Можно еще делать вот так: static bool cmp(pnt * a, pnt * b), тогда, если я правильно понимаю, стоимость свопа падает (и, кстати, это действительно дает еще небольшое ускорение, теперь тормозят new), но все равно stable_sort быстрее (почему?...). Вот еще ссылочка http://attractivechaos.wordpress.com/2008/08/28/comparison-of-internal-sorting-algorithms/

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

        Спасибо:) Именно на ответ такого рода я и ждал. В свободное время надо будет поэкспериментировать — покормить ее сортированными/шафленными массивами и остальное в этом духе.

        И статья интересная)

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

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

        Ну, утверждать не берусь, смотреть лень, но, видимо, в MSC++ всегда используется merge_sort Это не так. Везде в современных реализациях STL в std::sort используются вариации introsort.

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

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

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

    Тут пары сравниваются, так что в структуре не определишь. Да, и у меня почему-то перегруженный оператор< для пар работает дольше чем функция-компаратор.

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

      так пара сравнивает с начала первый операнд, затем второй, а при сравнении в данном случае второй вообще не затрагивается

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

      если хочется скорости, то лучше вообще функтор делать

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

        А чем функтор лучше простой inline-функции с точки зрения оптимизации?

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

          а как ты инлайнить будешь указатель на функцию при компиляции?

          template<class T>
          f(T g)
          { g(); }
          

          если T это конкретный класс, то всё понятно что инлайнить, а если T указатель на функцию void (*)()?

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

            То, что инлайнить, понятно из места вызова, ведь функция inline, а значит, определена в том же файле.

            Код: http://pastie.org/7155607

            Результат компиляции с gcc -O2:

            _Z6test_av:
                movl    $42, %eax
                ret
            
            _Z6test_bv:
                movl    $42, %eax
                ret
»
12 лет назад, # |
  Проголосовать: нравится +14 Проголосовать: не нравится

забавно, если перед вызовом sort поставить

srand(time(0));
random_shuffle(v.begin(), v.end());

то время выполнения становится одинаковым, то есть на GCC быстрее