С# to С++ dictionary to unordered_map результаты

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

Я делал много чтения, но сегодня начал писать код.

На моей машине с Windows 7/64 бит, запускающий VS2010, я создал два проекта: 1) Проект С#, который позволяет мне писать вещи так, как я привык. 2) Проект "makefile" С++, который позволяет мне играть, пытаясь реализовать одно и то же. Из того, что я понимаю, это НЕ проект .NET.

Мне нужно заполнить словарь значениями 10K. По какой-то причине С++ на несколько порядков медленнее.

Здесь С# ниже. Примечание. Я положил функцию после измерения времени, чтобы убедиться, что компилятор не "оптимизирован":

var freq = System.Diagnostics.Stopwatch.Frequency;

int i;
Dictionary<int, int> dict = new Dictionary<int, int>();
var clock = System.Diagnostics.Stopwatch.StartNew();

for (i = 0; i < 10000; i++)
     dict[i] = i;
clock.Stop();

Console.WriteLine(clock.ElapsedTicks / (decimal)freq * 1000M);
Console.WriteLine(dict.Average(x=>x.Value));
Console.ReadKey(); //Don't want results to vanish off screen

Здесь С++, не так много думал об этом (пытаясь узнать, правильно?)   int input;

LARGE_INTEGER frequency;        // ticks per second
LARGE_INTEGER t1, t2;           // ticks
double elapsedTime;

// get ticks per second
QueryPerformanceFrequency(&frequency);

int i;
boost::unordered_map<int, int> dict;
// start timer
QueryPerformanceCounter(&t1);

for (i=0;i<10000;i++)
    dict[i]=i;

// stop timer
QueryPerformanceCounter(&t2);

// compute and print the elapsed time in millisec
elapsedTime = (t2.QuadPart - t1.QuadPart) * 1000.0 / frequency.QuadPart;
cout << elapsedTime << " ms insert time\n";
int input;
cin >> input; //don't want console to disappear

Теперь, некоторые оговорки. Мне удалось найти этот связанный вопрос SO. Один из парней написал длинный ответ, в котором упоминалось, что WOW64 искажает результаты. Я установил проект для выпуска и перешел на вкладку "Свойства" проекта С++, включив все, что звучало так, как будто бы это ускорилось. Изменена платформа для x64, хотя я не уверен, что это касается его проблемы с wow64. Я не настолько разбираюсь в настройках компилятора, возможно, у вас, ребята, есть больше намека?

О, и результаты: С#: 0.32ms С++: 8.26ms. Это немного странно. Я что-то неправильно истолковал о чем. Я скопировал код таймера С++ из какого-либо места в Интернете, пройдя всю установку boost и включив /libfile rigmarole. Или, может быть, я действительно использую разные инструменты невольно? Или там какой-то критический параметр компиляции, который я не использовал? Или, может быть, код С# оптимизирован, потому что среднее значение является константой?

Здесь строка командной строки С++, на странице свойств → C/С++ → Command Line: /I "C:\Users\Carlos\Desktop\boost_1_47_0" /Zi/nologo/W 3/WX-/MP/Ox/Oi/Ot/GL/D "_MBCS" /Gm -/EHsc/GS-/Gy-/arch: SSE2/fp: fast/Zc: wchar_t/Zc: forScope/Fp "x64\Release\MakeTest.pch" /Fa "x64\Release\" /Fo "x64\Release\" /Fd "x64\Release\vc100.pdb" /Gd/errorReport: queue

Любая помощь будет оценена, спасибо.

Ответ 1

Простое изменение распределителя сократит это время.

boost::unordered_map<int, int, boost::hash<int>, std::equal_to<int>, boost::fast_pool_allocator<std::pair<const int, int>>> dict;

0,9 мс в моей системе (от 10 мс до). Это говорит о том, что на самом деле огромное, подавляющее большинство вашего времени вообще не расходуется в хеш-таблице, а в распределителе. Причина, по которой это несправедливое сравнение, заключается в том, что ваш GC никогда не будет собираться в такой тривиальной программе, что дает ему неоправданное преимущество в производительности, а собственные распределители делают значительное кэширование свободной памяти - но это никогда не вступит в игру в таком тривиальном например, потому что вы никогда не выделяли или не освобождали что-либо, и поэтому ничего не кэшировать.

Наконец, реализация пула Boost является потокобезопасной, в то время как вы никогда не играете с потоками, поэтому GC может просто вернуться к однопоточной реализации, которая будет намного быстрее.

Я прибегнул к ручному, несвободному, не потоковому распределению пула, и опустился до 0.525ms для С++ до 0.45ms для С# (на моей машине). Вывод: ваши исходные результаты были очень искажены из-за различных схем распределения памяти двух языков, и как только это было разрешено, разница становится относительно минимальной.

Пользовательский хэшер (как описано в ответе Александра) снизил время моего С++ до 0,34 мс, что теперь быстрее, чем С#.

static const int MaxMemorySize = 800000;
static int FreedMemory = 0;
static int AllocatorCalls = 0;
static int DeallocatorCalls = 0;
template <typename T>
class LocalAllocator
{
  public:
      std::vector<char>* memory;
      int* CurrentUsed;
      typedef T value_type;
      typedef value_type * pointer;
      typedef const value_type * const_pointer;
      typedef value_type & reference;
      typedef const value_type & const_reference;
      typedef std::size_t size_type;
      typedef std::size_t difference_type;

    template <typename U> struct rebind { typedef LocalAllocator<U> other; };

    template <typename U>
    LocalAllocator(const LocalAllocator<U>& other) {
        CurrentUsed = other.CurrentUsed;
        memory = other.memory;
    }
    LocalAllocator(std::vector<char>* ptr, int* used) {
        CurrentUsed = used;
        memory = ptr;
    }
    template<typename U> LocalAllocator(LocalAllocator<U>&& other) {
        CurrentUsed = other.CurrentUsed;
        memory = other.memory;
    }
    pointer address(reference r) { return &r; }
    const_pointer address(const_reference s) { return &r; }
    size_type max_size() const { return MaxMemorySize; }
    void construct(pointer ptr, value_type&& t) { new (ptr) T(std::move(t)); }
    void construct(pointer ptr, const value_type & t) { new (ptr) T(t); }
    void destroy(pointer ptr) { static_cast<T*>(ptr)->~T(); }

    bool operator==(const LocalAllocator& other) const { return Memory == other.Memory; }
    bool operator!=(const LocalAllocator&) const { return false; }

    pointer allocate(size_type count) {
        AllocatorCalls++;
        if (*CurrentUsed + (count * sizeof(T)) > MaxMemorySize)
            throw std::bad_alloc();
        if (*CurrentUsed % std::alignment_of<T>::value) {
            *CurrentUsed += (std::alignment_of<T>::value - *CurrentUsed % std::alignment_of<T>::value);
        }
        auto val = &((*memory)[*CurrentUsed]);
        *CurrentUsed += (count * sizeof(T));
        return reinterpret_cast<pointer>(val);
    }
    void deallocate(pointer ptr, size_type n) {
        DeallocatorCalls++;
        FreedMemory += (n * sizeof(T));
    }

    pointer allocate() {
        return allocate(sizeof(T));
    }
    void deallocate(pointer ptr) {
        return deallocate(ptr, 1);
    }
};
int main() {
    LARGE_INTEGER frequency;        // ticks per second
    LARGE_INTEGER t1, t2;           // ticks
    double elapsedTime;

    // get ticks per second
    QueryPerformanceFrequency(&frequency);
    std::vector<char> memory;
    int CurrentUsed = 0;
    memory.resize(MaxMemorySize);

    struct custom_hash {
        size_t operator()(int x) const { return x; }
    };
    boost::unordered_map<int, int, custom_hash, std::equal_to<int>, LocalAllocator<std::pair<const int, int>>> dict(
        std::unordered_map<int, int>().bucket_count(),
        custom_hash(),
        std::equal_to<int>(),
        LocalAllocator<std::pair<const int, int>>(&memory, &CurrentUsed)
    );

    // start timer
    std::string str;
    QueryPerformanceCounter(&t1);

    for (int i=0;i<10000;i++)
        dict[i]=i;

    // stop timer
    QueryPerformanceCounter(&t2);

    // compute and print the elapsed time in millisec
    elapsedTime = ((t2.QuadPart - t1.QuadPart) * 1000.0) / frequency.QuadPart;
    std::cout << elapsedTime << " ms insert time\n";
    int input;
    std::cin >> input; //don't want console to disappear
}

Ответ 2

Вы можете попробовать dict.rehash(n) с различными (большими) значениями n перед вставкой элементов и посмотреть, как это влияет на производительность. Распределения памяти (они происходят, когда контейнер заполняет ведра), как правило, более дороги в С++, чем на С#, а перехват также тяжелый. Для std::vector и std::deque аналоговая функция-член reserve.

Различные политики переопределения и порог коэффициента загрузки (посмотрите на функцию члена max_load_factor) также сильно повлияют на производительность unordered_map.

Далее, поскольку вы используете VS2010, я предлагаю вам использовать std::unordered_map из заголовка <unordered_map>. Не используйте boost, когда вы можете использовать стандартную библиотеку.

Фактическая используемая функция хеширования может сильно повлиять на производительность. Вы можете попробовать следующее:

struct custom_hash { size_t operator()(int x) const { return x; } };

и используйте std::unordered_map<int, int, custom_hash>.

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

Ответ 3

Сохранение последовательной последовательности числовых встроенных клавиш, добавленных в порядке возрастания, определенно НЕ предназначено для хэш-таблиц.

Используйте массив или генерируйте случайные значения.

И сделайте несколько попыток. Таблицы хешей очень оптимизированы для поиска.

Ответ 4

Visual Studio TR1 unordered_map совпадает с stdext:: hash_map:

Другой поток, спрашивающий, почему он работает медленнее, см. мой ответ со ссылками на другие, которые обнаружили ту же проблему. Вывод заключается в использовании другой реализации hash_map, когда в С+++:

Альтернатива stdext:: hash_map по причинам производительности

Btw. помните, когда в С++ существует большая разница между оптимизированной версией Release-build и не оптимизированной Debug-build по сравнению с С#.