Без знака int или итератор при использовании вектора? - PullRequest
0 голосов
/ 30 ноября 2018

Я сейчас работаю над школьным проектом c ++ с некоторыми друзьями.

Раньше, когда у меня были векторы в c ++, я делал что-то вроде этого, чтобы использовать их:

unsigned int i = 0;
while (i != myVector.size())
{
    doSomething(myVector[i]);
    i++;
}

Но во время этогоПроект мои друзья не были рады видеть меня, используя такие векторы, и попросили меня использовать итераторы.Мне не очень нравятся итераторы, потому что их синтаксис довольно сложно запомнить, но мои друзья сказали, что лучше их использовать, потому что он работает быстрее.И поскольку мы работаем над большим проектом с большим количеством векторов, было важно использовать итераторы.

Время прошло, и я все еще использую их, даже если я до сих пор не могу вспомнить их синтаксис, но я хотелпосмотрим, был ли метод итератора действительно быстрее их, метод unsigned int.

Итак, я создал 2 программы:

Первая программа, использующая метод unsigned int:

#include <vector>
#include <string>
#include <iostream>

int main()
{
    std::string str = "This is a string";

    int i = 0;
    std::vector<std::string> vec;

    while (i != 10000000)
    {
        vec.push_back(str);
        i++;
    }

    unsigned int j = 0;
    while (j != vec.size())
    {
        std::cout << vec[j] << std::endl;
        j++;
    }
    return (0);
}

И вторая программа, использующая метод итератора:

#include <vector>
#include <string>
#include <iostream>

int main()
{
    std::string str = "This is a string";

    int i = 0;
    std::vector<std::string> vec;

    while (i != 10000000)
    {
        vec.push_back(str);
        i++;
    }

    std::vector<std::string>::iterator it;
    it = vec.begin();
    while (it != vec.end())
    {
        std::cout << *it << std::endl;
        it++;
    }
    return (0);
}

Как вы можете видеть, обе программы сначала создадут вектор размером 10 000 000 (я поставил большой размер, так что если естьразницу во времени будет легче заметить), а затем я просто напечатаю строку в векторе, но используя два разных метода.

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

time ./a.out

И вот результат:

Метод unsigned int:

real    0m39,391s
user    0m5,463s
sys     0m21,108s

Метод итератора:

real    0m39,436s
user    0m5,972s
sys     0m20,652s

И ......... это же время ?!Разница между ними незначительна, менее 1 секунды, и это вектор с 10 миллионами строк.

Так что мне было интересно, есть ли разница между этими двумя методами и действительно ли лучше использовать итераторы?

Ответы [ 2 ]

0 голосов
/ 30 ноября 2018

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

unsigned int i = 0;
while (i != myVector.size())
{
    doSomething(myVector[i]);
    i += 2;
}

или

unsigned int start = myVector.size() + 42;

for (unsigned int i = start; i != myVector.size(); ++i){
    doSomething(myVector[i]);
}

с

for (const auto& e : myVector) {
     doSomething(e);
}

Диапазон, основанный на циклах, делает использование итераторов настолько простым, насколько это возможно (вы даже не видитеитераторы, но они используются за кадром).Когда вы вручную управляете индексом, есть миллионы способов ошибиться, с итераторами их может быть 2 или 3.

Для сравнения производительности: поскольку векторы хранят свой элемент в непрерывной памяти, векторные итераторы могут быть простымиуказатели.То, что вы считаете накладными расходами, - это в основном синтаксический сахар, позволяющий писать более приятный код.Поэтому неудивительно, что вы не видите большой разницы.

PS

Я использовал это много, я уверен, что не слишком много ошибок

Я знаю это чувствоочень хорошо из моего собственного опыта.Не так давно мне показалось, что я достиг приемлемого уровня в тайном искусстве работы с массивами в стиле c, и я не хотел использовать std::vector, потому что был уверен, что он не может быть таким же эффективным, как необработанный массив.Я не мог быть более неправ, но это другая история.

Использование целого числа для итерации массива относится к прошлому веку.Это небезопасно, это приводит к трудностям в обнаружении ошибок и может легко вызывать неопределенное поведение.Напишите код, чтобы выразить, что вы хотите сделать, а не инструктировать ваш процессор.Если вы хотите что-то сделать для каждого элемента вектора, вы должны использовать std::for_each:

std::for_each(myVector.begin(),myVector.end(),doSomething);

У него нет никаких недостатков ручного использования индекса (вы заметили,ошибки в вышеуказанных циклах?) и имеет то преимущество, что выглядит одинаково, независимо от того, какой контейнер myVector на самом деле или какой тип элемента он содержит, или какой doSomething на самом деле (это может быть свободная функция, функторлямбда, твой выбор).

0 голосов
/ 30 ноября 2018

Удивительно, но при сравнении доступа к итератору с доступом к индексу в цикле может быть, по крайней мере, теоретическая разница в производительности, как показано здесь: https://gcc.godbolt.org/z/frFYhF

Фильтрация по некоторому шуму, с итератором каждая итерация выглядиткак

.LBB0_1:                                # =>This Inner Loop Header: Depth=1
    movl    (%rbx), %edi
    callq   check(int)
    addq    $4, %rbx
    cmpq    %rbx, %r14
    jne     .LBB0_1

Итак, здесь мы видим один доступ к памяти, одну математическую операцию и одну условную ветвь.В целом, доступ к памяти затмит все остальное, как только вы выйдете за пределы строки кэша, но все же это выполняемые операции.

Когда мы смотрим на доступ к индексу, итерация выглядит следующим образом:

.LBB1_3:                                # =>This Inner Loop Header: Depth=1
    movq    (%r14), %rax
    movl    (%rax,%rbx,4), %edi
    callq   check(int)
    addq    $1, %rbx
    cmpq    %r15, %rbx
    jb      .LBB1_3

Здесь мы видим то, чего не видим в предыдущем примере - дополнительное перемещение регистров на каждой итерации (что необходимо для доступа к памяти смещения).

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

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

Сказав все это, я предлагаю вам выбрать цикл на основе диапазона.

for (int i: vec) {
     // work with i
}

Последний, но не списокиспользование переменной unsigned int для перебора индексов вектора может быть неприятной ошибкой.На многих платформах вектор может быть больше, чем позволяет maxim int, и вы получите бесконечный цикл.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...