Подтвердить что ты не робот

Статический векторный макет внутренних данных - `union` vs` std:: aligned_storage_t` - огромная разница в производительности

Предположим, что вам нужно реализовать класс static_vector<T, N>, который является контейнером с фиксированной емкостью, который полностью живет в стеке и никогда не выделяет, и предоставляет интерфейс std::vector. (Boost обеспечивает boost::static_vector.)

Учитывая, что для максимальных N экземпляров T у нас должно быть неинициализированное хранилище, существует множество вариантов, которые можно сделать при разработке внутреннего расположения данных:

  • Одночастный union:

    union U { T _x; };
    std::array<U, N> _data;
    
  • Одиночный std::aligned_storage_t:

    std::aligned_storage_t<sizeof(T) * N, alignof(T)> _data;
    
  • Массив std::aligned_storage_t:

    using storage = std::aligned_storage_t<sizeof(T), alignof(T)>;
    std::array<storage, N> _data;
    

Независимо от выбора, для создания членов потребуется использование "размещение new" и доступ к ним потребует чего-то по строкам reinterpret_cast.


Теперь предположим, что у нас есть две очень минимальные реализации static_vector<T, N>:

  • with_union: реализован с использованием подхода с одним членом union;

  • with_storage: реализован с использованием подхода "single std::aligned_storage_t".

Позвольте выполнить следующий тест, используя как g++, так и clang++ с -O3. Я использовал quick-bench.com для этой задачи:

void escape(void* p) { asm volatile("" : : "g"(p) : "memory"); }
void clobber()       { asm volatile("" : :        : "memory"); }

template <typename Vector>
void test()
{
    for(std::size_t j = 0; j < 10; ++j)
    {
        clobber();
        Vector v;
        for(int i = 0; i < 123456; ++i) v.emplace_back(i);
        escape(&v);
    }
}

(escapeи clobber взяты из Chandler Carruth CppCon 2015 talk: "Настройка С++: тесты, и процессоры, и компиляторы! Oh My!" )

g++ results

  • Результаты для clang++ 5.0 (live here):

clang++ results


Как вы можете видеть из результатов, g++, похоже, может агрессивно оптимизировать (векторизация) реализацию, использующую подход "single std::aligned_storage_t", но не реализацию с помощью union.

clang++ не может агрессивно оптимизировать любой из них.

Мои вопросы:

  • Есть ли что-нибудь в стандарте, которое предотвращает агрессивную оптимизацию реализации с помощью union? (Т.е. стандарт предоставляет больше свободы компилятору при использовании std::aligned_storage_t - если так почему?)

  • Является ли это исключительно вопросом "качества реализации"?

4b9b3361

Ответ 1

xskxzr прав, это та же проблема, что и в этом вопросе. По сути, gcc не имеет возможности оптимизации, забывая, что данные std::array выровнены. Джон Звинк успешно сообщил ошибка 80561.

Вы можете проверить это в своем тесте, сделав одно из двух изменений в with_union:

  • Измените _data с std::array<U, N> на просто a U[N]. Производительность становится идентичной.

  • Напомните gcc, что _data фактически выравнивается, изменяя реализацию emplace_back() на:

    template <typename... Ts> 
    T& emplace_back(Ts&&... xs)
    {
        U* data = static_cast<U*>(__builtin_assume_aligned(_data.data(), alignof(U)));
        T* ptr = &data[_size++]._x;
        return *(new (ptr) T{std::forward<Ts>(xs)...});
    }
    

Любая из этих изменений с остальной частью вашего теста дает мне сопоставимые результаты между WithUnion и WithAlignedStorage.