Почему std::vector:: operator [] в 5-10 раз быстрее, чем std::vector:: at()?

Во время оптимизации программы, пытаясь оптимизировать цикл, который выполняет итерацию через вектор, я обнаружил следующий факт: std::vector:: at() EXTREMELY медленнее, чем оператор []!

Оператор [] в 5-10 раз быстрее, чем при(), как в версиях релиза, так и для отладки (VS2008 x86).

Чтение немного в Интернете заставило меня понять, что at() имеет проверку границ. Хорошо, но, замедляя работу до 10 раз?!

Есть ли причина для этого? Я имею в виду, что проверка границ - это простое сравнение чисел, или я что-то не хватает?
Вопрос в том, какова реальная причина этого удара производительности?
Более того, есть ли способ сделать это еще быстрее?

Я обязательно поменю все мои вызовы() с помощью [] в других частях кода (в которых у меня уже есть пользовательская проверка границ!).

Доказательство концепции:

#define _WIN32_WINNT 0x0400
#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#include <conio.h>

#include <vector>

#define ELEMENTS_IN_VECTOR  1000000

int main()
{
    __int64 freq, start, end, diff_Result;
    if(!::QueryPerformanceFrequency((LARGE_INTEGER*)&freq))
        throw "Not supported!";
    freq /= 1000000; // microseconds!

    ::std::vector<int> vec;
    vec.reserve(ELEMENTS_IN_VECTOR);
    for(int i = 0; i < ELEMENTS_IN_VECTOR; i++)
        vec.push_back(i);

    int xyz = 0;

    printf("Press any key to start!");
    _getch();
    printf(" Running speed test..\n");

    { // at()
        ::QueryPerformanceCounter((LARGE_INTEGER*)&start);
        for(int i = 0; i < ELEMENTS_IN_VECTOR; i++)
            xyz += vec.at(i);
        ::QueryPerformanceCounter((LARGE_INTEGER*)&end);
        diff_Result = (end - start) / freq;
    }
    printf("Result\t\t: %u\n\n", diff_Result);

    printf("Press any key to start!");
    _getch();
    printf(" Running speed test..\n");

    { // operator []
        ::QueryPerformanceCounter((LARGE_INTEGER*)&start);
        for(int i = 0; i < ELEMENTS_IN_VECTOR; i++)
            xyz -= vec[i];
        ::QueryPerformanceCounter((LARGE_INTEGER*)&end);
        diff_Result = (end - start) / freq;
    }

    printf("Result\t\t: %u\n", diff_Result);
    _getch();
    return xyz;
}

Edit:
Теперь значение присваивается "xyz", поэтому компилятор не будет "вытирать" его.

Ответ 1

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

Если вы можете доказать, что индекс находится в пределах диапазона без проверки времени выполнения, используйте operator[]. В противном случае используйте at() или добавьте собственную проверку перед доступом. operator[] должен быть как можно более быстрым, но будет беспорядочно взорваться, если индекс недействителен.

Ответ 2

Я запустил тестовый код на своей машине:

В неоптимизированной сборке отладки разница между двумя циклами невелика.

В оптимизированной версии релиза второй цикл цикла полностью оптимизирован (вызов operator[], скорее всего, встроен, и оптимизатор может видеть, что цикл ничего не делает и может удалить весь цикл).

Если я изменяю тело циклов, чтобы выполнить некоторую фактическую работу, например, vec.at(i)++; и vec[i]++;, соответственно, разница между двумя циклами незначительна.

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

Ответ 3

Вы ничего не делаете с возвращаемым значением, поэтому, если компилятор строит эти функции, он может полностью их оптимизировать. Или, возможно, он полностью оптимизирует версию индекса ([]). Выполнение без оптимизации бесполезно с точки зрения измерения производительности, вам нужна простая, но полезная программа для реализации функций, чтобы они не просто оптимизировались. Например, вы можете перетасовать вектор (произвольно обменивать 50000 пар элементов).