Pull to refresh

Comments 44

А оптимизация никак не может на этот рейт влиять?
У меня на тестах, при оптимизации O1 и O3 результаты по rate одинаковые. Параметры передаются по значению и скорее всего компилятор не может с этим ничего поделать.
Почему-то так я и подумал. Спасибо!
Компилятор не может с этим ничего поделать, поскольку у вас в конструкторах/деструкторах код, который влияет на результат вычислений (программы). Пустые конструкторы/деструкторы компилятор вполне мог бы вырезать. Надо объектный код смотреть.
Когда конструкторы и деструкторы пустые, можно и не волноваться. А когда они есть, и в них что-то сложное, тогда всё только ухудшается. И компилятор ничего не сделает, да и функции дорогие. :-)
rate зависит от степени отсортированности массива, это видно, если наполнять массив разными данными.
Я, честно говоря, ожидал увидеть логарифмическую зависимость, но пока её наблюдать не приходится.
Код
#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip>

using namespace std;

class Compare
{
public:

    static int num;

    Compare()
    {
      num++;
    }

    Compare(const Compare& compare)
    {
      num++;
    }

    ~Compare()
    {
    }
  
    bool operator() (int v1, int v2)
    {
      return v1 < v2;
    }
};
int Compare::num = 0;

int main()
{
    vector<int> mas;
    
    for(int step = 1; step <= 3; ++step){
    
      cout << "---- step " << step << endl;

      for(int size = 1; size < 1e7; size *= 2){
        
        mas.resize(size);
        for( int k = 0; k < size; k++ ) mas[k] = rand();

        Compare::num = 0;
        sort( mas.begin(), mas.end(), Compare() );

        cout << setw(7) << size << setw(7) << setprecision(4) << ((float)Compare::num/(float)size) << endl;
      }
    }

    return 0;
}

Результат выполнения: размер массива - rate
>g++ habrasort.cpp -o habrasort && habrasort
---- step 1
      1      4
      2      2
      4      1
      8      1
     16  1.125
     32  1.313
     64  1.422
    128  1.422
    256  1.477
    512  1.441
   1024  1.431
   2048  1.452
   4096  1.448
   8192  1.456
  16384  1.451
  32768  1.442
  65536  1.437
 131072  1.428
 262144  1.413
 524288   1.39
1048576   1.38
2097152  1.367
4194304  1.362
8388608  1.359
---- step 2
      1      4
      2    2.5
      4    1.5
      8  1.125
     16 0.9375
     32   1.25
     64  1.391
    128  1.469
    256  1.406
    512  1.443
   1024  1.454
   2048  1.444
   4096   1.45
   8192   1.46
  16384  1.456
  32768  1.449
  65536  1.438
 131072  1.434
 262144  1.412
 524288   1.39
1048576   1.38
2097152  1.368
4194304  1.362
8388608  1.359
---- step 3
      1      4
      2      2
      4    1.5
      8  1.375
     16  1.063
     32  1.219
     64  1.359
    128  1.391
    256  1.395
    512  1.486
   1024  1.449
   2048  1.462
   4096  1.446
   8192  1.465
  16384  1.448
  32768  1.443
  65536  1.438
 131072  1.428
 262144  1.413
 524288   1.39
1048576  1.381
2097152  1.367
4194304  1.362
8388608  1.359

>g++ habrasort.cpp -o habrasort -O3 && habrasort
---- step 1
      1      4
      2      2
      4      1
      8      1
     16  1.125
     32  1.313
     64  1.422
    128  1.422
    256  1.477
    512  1.441
   1024  1.431
   2048  1.452
   4096  1.448
   8192  1.456
  16384  1.451
  32768  1.442
  65536  1.437
 131072  1.428
 262144  1.413
 524288   1.39
1048576   1.38
2097152  1.367
4194304  1.362
8388608  1.359
---- step 2
      1      4
      2    2.5
      4    1.5
      8  1.125
     16 0.9375
     32   1.25
     64  1.391
    128  1.469
    256  1.406
    512  1.443
   1024  1.454
   2048  1.444
   4096   1.45
   8192   1.46
  16384  1.456
  32768  1.449
  65536  1.438
 131072  1.434
 262144  1.412
 524288   1.39
1048576   1.38
2097152  1.368
4194304  1.362
8388608  1.359
---- step 3
      1      4
      2      2
      4    1.5
      8  1.375
     16  1.063
     32  1.219
     64  1.359
    128  1.391
    256  1.395
    512  1.486
   1024  1.449
   2048  1.462
   4096  1.446
   8192  1.465
  16384  1.448
  32768  1.443
  65536  1.438
 131072  1.428
 262144  1.413
 524288   1.39
1048576  1.381
2097152  1.367
4194304  1.362
8388608  1.359


UPD: Заполнил массив как for( int k = 0; k < size; k++ ) mas[k] = 1e7 - k; — получил число, стремящееся к 5.
Кстати, 1.359*2 = 2.718. Я тоже сначала думал о логарифмической зависимости, но получалось константа. Возможно, тут получается сумма какого-то рада из логарифмов, значение которого e/2.
К UPD про rate=5. Сложность алгоритма sort в среднем O(N*logN), а в худшем случае O(N^2). Тут похоже эффект от «плохого» массива.
До квадрата дело тут не дошло. 8 миллионов элементов так быстро бы не отсортировалось.
Запустил ваш код в 2013 студии с полной оптимизацией.

Результат
---- step 1
      1      2
      2      2
      4      1
      8    0.5
     16   0.25
     32  0.125
     64 0.4375
    128 0.3594
    256 0.3438
    512 0.3652
   1024  0.373
   2048 0.3711
   4096 0.4006
   8192 0.3885
  16384 0.3855
  32768 0.3741
  65536 0.3645
 131072 0.3445
 262144 0.3098
 524288 0.2511
1048576 0.1682
2097152 0.1093
4194304 0.05469
8388608 0.02734

---- step 2
      1      2
      2      2
      4      1
      8    0.5
     16   0.25
     32  0.125
     64 0.2969
    128 0.3359
    256 0.3906
    512 0.3789
   1024 0.3701
   2048 0.3916
   4096 0.3936
   8192 0.3824
  16384 0.3782
  32768 0.3759
  65536 0.3668
 131072 0.3447
 262144 0.3085
 524288 0.2512
1048576 0.1682
2097152 0.1093
4194304 0.05469
8388608 0.02734

---- step 3
      1      2
      2      2
      4      1
      8    0.5
     16   0.25
     32  0.125
     64 0.2969
    128 0.3828
    256 0.4375
    512 0.3652
   1024  0.373
   2048  0.396
   4096  0.395
   8192  0.385
  16384 0.3817
  32768 0.3804
  65536 0.3666
 131072 0.3458
 262144 0.3095
 524288 0.2514
1048576  0.168
2097152 0.1093
4194304 0.05469
8388608 0.02734

У меня нет под рукой VS 2013. Попробуйте заменить rand() на rand()*32768+rand().
Заменил, вот результат:
Результат с rand()*32768+rand()
---- step 1
      1      2
      2      2
      4      1
      8    0.5
     16   0.25
     32  0.125
     64 0.3438
    128 0.4531
    256 0.4141
    512 0.3535
   1024 0.3877
   2048 0.3799
   4096 0.3833
   8192 0.3851
  16384  0.383
  32768 0.3881
  65536 0.3911
 131072 0.3884
 262144 0.3914
 524288 0.3899
1048576 0.3899
2097152 0.3896
4194304 0.3901
8388608 0.3901
---- step 2
      1      2
      2      2
      4      1
      8    0.5
     16   0.25
     32  0.125
     64 0.2969
    128 0.3359
    256 0.3555
    512  0.377
   1024 0.4082
   2048 0.3843
   4096 0.3877
   8192 0.3982
  16384 0.3973
  32768   0.39
  65536 0.3889
 131072 0.3894
 262144 0.3907
 524288 0.3897
1048576 0.3899
2097152 0.3902
4194304 0.3901
8388608 0.3899
---- step 3
      1      2
      2      2
      4      1
      8    0.5
     16   0.25
     32  0.125
     64 0.3438
    128 0.3594
    256 0.3438
    512 0.3945
   1024  0.376
   2048 0.3828
   4096  0.396
   8192 0.3813
  16384  0.389
  32768 0.3897
  65536 0.3906
 131072 0.3906
 262144 0.3899
 524288 0.3908
1048576 0.3903
2097152 0.3904
4194304 0.3897
8388608 0.3899


Скорее всего это связано с тем что в С++ 11 поменяли реализацию, об этом указано на en.cppreference.com/w/cpp/algorithm/sort
а именно:

Complexity
until C++11: O(N·log(N)), where N = std::distance(first, last) comparisons on average.

since C++11: O(N·log(N)), where N = std::distance(first, last) comparisons.
Интересно… Можно отличать по этому признаку компиляторы (до их первого обновления).

На данный момент в gcc 4.8.1 такие результаты (с флагом -std=c++11 не меняются):
размер | rate       | массив M
-------+------------+------------------------
     1 |          4 | любой
  >> 1 |       1.25 | M[i] == M[j]
  >> 1 |       1.33 | M[i] < M[i+1]
  >> 1 |          5 | M[i] > M[i+1]
  >> 1 | около 1.39 | M[i] == i % 2 ? 10 : 11
  >> 1 | около 1.34 | M[i] == i % 3 ? 10 : 11

(«около» — значит нельзя уверенно сказать, что значение к чему-то стремится)
Попробуйте для интереса тоже забить константу/возрастающую/убывающую последовательность.
>> for_each( mas.begin(), mas.end(), Print() );
Странно я всегда полагал что Print() создаcт анонимный объект, а при заходе в for_each произойдет его копирование. Это какой — то результат оптимизации, что с++ анонимные объекты передает принудительно по ссылке или стандарт?
А зачем создавать объект и делать его копирование, если он никому больше, как в самой функции, не нужен? Можно сразу там его один раз и создать.
Я могу согласиться что в этом нет смысла, но С++ не всегда делает то, что кажется нам разумным.
В C++ есть правило «as if» — компилятор может делать все, что угодно, если наблюдаемое поведение соответствует семантике стандарта. Если у вас нет кода, в котором можно наблюдать разницу между копированием и его отсутствием, то оптимизатор вправе заменить одно на другое.
UFO just landed and posted this here
Это стандарт. Оптимизация copy elision. Работает только с анонимными объектами или при возвращении значений из функции. (http://en.cppreference.com/w/cpp/language/copy_elision)
Во-первых, функторы — это из теории категорий. А то, что в статье описано — это функциональные объекты.

Во-вторых, в статье напрашивается рассмотреть move семантику и перемещающие конструкторы.

Ну и в-третьих, писать свои функциональные объекты — это довольно унылое занятие, когда есть bind и лямбды, а печать так вообще делается как

std::copy(mas.cbegin(), mas.cend(), std::ostream_iterator<int>(std::cout, " "));
Согласен, но не хотелось затрагивать С++11. Думаю, начинающим надо давать материал постепенно и попроще. Такая запись
std::ostream_iterator<int>(std::cout, " ")

многих отпугнет.
Для начинающих C++11 как раз проще, там не надо особо объяснять, как работают лямбды, да и граблей меньше. А в компиляторах они уже во всех есть несколько лет как.
В рамках C++, «functors» — это таки вполне устоявшееся название, пусть и неудачное.
Стандарт их называет функциональными объектами (function objects). В статье, я считаю, нужно писать правильно.
И отучать русских программистов от неудачного сленга. Кто-то в дремучем году ляпнул (не Александреску ли?), и понеслось.
К сожалению, «функтор» — чертовски перегруженное слово в русском языке.

В С++ ими традиционно обзывают классы с оператором (); не всякие функциональные объекты, а именно классы; потому что указатели на свободные функции, а также синтаксическое замыкание указателя на объект и указателя на функцию-член (ptr->*member), — не являются функторами.
Вслед за плюсами — функторами обзывают функциональные объекты и в других языках.
В теоркате — отображения категорий с определённой аксиоматикой (выполняется ковариантность)
В хаскелле — тайпкласс Functor — интерфейс, воспроизводящий теоркатовские функторы. Ну и реализации этого тайпкласса для параметризованных типов.
В окамле — параметризованные модули — отображения типов и модулей (по сути, категорий) вообще как бог на душу положит.
Интересный момент про создание копий функторов, не знал об этом. Пишут что это проблема библиотечного хидера algorithm в целом. В принципе она зависит от имплементации (некоторые версии STL могут быть свободны от этого косяка), но в большинстве имплементаций создаются копии.

В общем случае проблема лечится использованием std::reference_wrapper

Compare sort_criterion; sort( mas.begin(), mas.end(), std::cref(sort_criterion) );
Сортировка же зачастую рекурсивный алгоритм, логично предположить, что ваш функциональный объект будет передан в следующий рекурсивный вызов. Поскольку объект принимается по значению, то он копируется до тех пор, пока вы явно не попросите этого не делать. Здесь в игру и вступает reference_wrapper.
А как в этом случае себя будут вести лямбда функции?
Надо полагать, что как указатели на функции, если ничего не захватывают из области видимости, откуда создаются; и как объекты с конструкторами по умолчанию в другом лсучае.
Во-первых, в C++ нет никаких лямбда-функций, есть лямбда-выраженин, и генерируемые из них замыкания.
Если лямбда выражение без захвата, то у типа-замыкания определён non-explicit оператор приведения к указателю на функцию с соответствующей сигнатурой.
Поскольку алгоритмы принимают действие с выводом типов, приведение к указателю на функцию в них происходить не может, происходит обычное копирование объекта, но поскольку он пустой, это в целом равносильно копированию указателя на функцию.
Помимо вышесказанного, за счёт встраивания копирования в результате вообще может не быть, и в этом можно убедиться, сравнив машинный код для обычного for по итератору, для range-based for и для std::for_each с замыканием.
Если бы это было не так, то алгоритмы вроде for_each никто бы не использовал из-за abstraction penalty.
Кстати, при использовани libc++ (это реализация стандартной библиотеки в Clang), rate получается очень маленьким.
Укажите компилятор, только из изучения комментариев понял что у вас vs2013. «починили» стандартную библиотеку? надо посмотреть в код, а то раньше там параметр шаблона не передавался по цепочке вложенному вызову, что приводило к забавным результатам
Попробуйте добавить const это укажет компилятору что вызов метода не изменит передаваемый экземпляр функтора и копировать его нет необходимости, он ведь все равно не изменится :)

void operator() (int value) const

ПС: Я не проверял, просто гипотеза
Не работает, я проверял :)
Добавил const для случая с сортировкой — результат сохраняется как и у 0serg. Скорее всего компилятор и так понял нас правильно.
Для интереса решил дело усложнить, добавив в operator() счётчик его вызовов. Количество вызовов конструктора осталось прежним. Количество вызовов operator() — O(NlnN), ничего противоестественного не произошло.
Графики
ctr_rate — вызовы конструктора на один элемент, cmp_rate — вызовы operator() на один элемент.

Сортировка убывающей последовательности:


Сортировка массива из нулей:


Сортировка случайного массива:
Из этой статьи, на примере for_each, я узнал, что в шаблон в качестве класса можно передавать функцию. Это всегда так было?
Нет, в качестве класса можно только передать класс-функтор, обычно у которого переопределён оператор (). Но в качестве параметра шаблона может быть не только класс, но и любое скалярное значение, включая функцию.
Точнее, указатель на неё.
Это одно и то же :)

#include <iostream>
using namespace std;
void foo()
{
	std::cout << "bar\n";
}

int main() {
	(*&**&**&*&****&*foo)();
	return 0;
}
Это не одно и то же, читайте что такое function-to-pointer conversion в ISO CPP.
Простой пример:
using function_type = void();
function_type a[10]; // compile-time error, cannot declare array of function type
function_type* a[10];
Действительно, параметром шаблона может быть функция, а не только указатель на неё. Не знал.
Sign up to leave a comment.

Articles