现代化方法制作std::vector分配对齐内存

21

此问题有关,但回答已经过时,用户Marc Glisse的评论表明自C++17以来有新方法可以解决这个问题,可能尚未充分讨论。

我正在尝试使SIMD适配器正常工作,同时仍然可以访问所有数据。

在Intel上,如果我创建一个类型为__m256的float向量,并将我的大小减少8倍,它会给我对齐的内存。

例如:std::vector<__m256> mvec_a((N*M)/8);

通过略微hacky的方式,我可以将指向向量元素的指针转换为float,从而使我能够访问单个float值。

相反,我希望拥有一个正确对齐的std::vector<float>,因此可以加载到__m256和其他SIMD类型中,而不会出现段错误。

我一直在研究aligned_alloc

这可以给我一个正确对齐的C风格数组:

auto align_sz = static_cast<std::size_t> (32);
float* marr_a = (float*)aligned_alloc(align_sz, N*M*sizeof(float));

然而,我不确定如何对std::vector<float>这样做。让std::vector<float>拥有marr_a的所有权似乎不可能

我看到一些建议说我应该编写自定义配置器,但这似乎是很多工作,或许在现代C++中还有更好的方法?


1
不要使用_mm256_loadu_ps(&vec[i]),以避免出现段错误或缓存行分裂导致的潜在减速情况。请注意,默认调优选项下,GCC会将未对齐256位加载/存储拆分为vmovups xmm / vinsertf128。因此,如果您关心代码在GCC上编译时是否有人忘记使用“-mtune = ...”或“-march =”选项,那么使用_mm256_load而不是loadu是有优势的。 - Peter Cordes
@PrunusPersica 你最终解决了这个问题吗?我也遇到了同样的问题。如果你愿意,我们可以一起合作解决。 - gansub
1
@gansub 我最终使用了 boost::alignment::aligned_allocator 的代码。然后我可以使用 std::vector<T, aligned_allocator<float>> 分配向量。这确实使得普通的 std::vectors 不能直接兼容这种类型的对齐向量,但你总是可以想办法解决这个问题。 - Prunus Persica
2个回答

7

STL容器带有一个allocator模板参数,可用于对齐其内部缓冲区。指定的allocator类型必须实现至少allocatedeallocatevalue_type

这些答案相比,此分配器的实现避免了依赖于平台的对齐malloc调用。相反,它使用C++17对齐的new操作符

此处是在godbolt上的完整示例。

#include <limits>
#include <new>

/**
 * Returns aligned pointers when allocations are requested. Default alignment
 * is 64B = 512b, sufficient for AVX-512 and most cache line sizes.
 *
 * @tparam ALIGNMENT_IN_BYTES Must be a positive power of 2.
 */
template<typename    ElementType,
         std::size_t ALIGNMENT_IN_BYTES = 64>
class AlignedAllocator
{
private:
    static_assert(
        ALIGNMENT_IN_BYTES >= alignof( ElementType ),
        "Beware that types like int have minimum alignment requirements "
        "or access will result in crashes."
    );

public:
    using value_type = ElementType;
    static std::align_val_t constexpr ALIGNMENT{ ALIGNMENT_IN_BYTES };

    /**
     * This is only necessary because AlignedAllocator has a second template
     * argument for the alignment that will make the default
     * std::allocator_traits implementation fail during compilation.
     * @see https://dev59.com/Eafja4cB1Zd3GeqP2-li#48062758
     */
    template<class OtherElementType>
    struct rebind
    {
        using other = AlignedAllocator<OtherElementType, ALIGNMENT_IN_BYTES>;
    };

public:
    constexpr AlignedAllocator() noexcept = default;

    constexpr AlignedAllocator( const AlignedAllocator& ) noexcept = default;

    template<typename U>
    constexpr AlignedAllocator( AlignedAllocator<U, ALIGNMENT_IN_BYTES> const& ) noexcept
    {}

    [[nodiscard]] ElementType*
    allocate( std::size_t nElementsToAllocate )
    {
        if ( nElementsToAllocate
             > std::numeric_limits<std::size_t>::max() / sizeof( ElementType ) ) {
            throw std::bad_array_new_length();
        }

        auto const nBytesToAllocate = nElementsToAllocate * sizeof( ElementType );
        return reinterpret_cast<ElementType*>(
            ::operator new[]( nBytesToAllocate, ALIGNMENT ) );
    }

    void
    deallocate(                  ElementType* allocatedPointer,
                [[maybe_unused]] std::size_t  nBytesAllocated )
    {
        /* According to the C++20 draft n4868 § 17.6.3.3, the delete operator
         * must be called with the same alignment argument as the new expression.
         * The size argument can be omitted but if present must also be equal to
         * the one used in new. */
        ::operator delete[]( allocatedPointer, ALIGNMENT );
    }
};

这个分配器可以像这样使用:

#include <iostream>
#include <stdexcept>
#include <vector>

template<typename T, std::size_t ALIGNMENT_IN_BYTES = 64>
using AlignedVector = std::vector<T, AlignedAllocator<T, ALIGNMENT_IN_BYTES> >;

int
main()
{
    AlignedVector<int, 1024> buffer( 3333 );
    if ( reinterpret_cast<std::uintptr_t>( buffer.data() ) % 1024 != 0 ) {
        std::cerr << "Vector buffer is not aligned!\n";
        throw std::logic_error( "Faulty implementation!" );
    }

    std::cout << "Successfully allocated an aligned std::vector.\n";
    return 0;
}

2
C++17支持超对齐动态分配,例如std::vector<__m256i>应该可以正常工作。有没有办法利用它的优点,而不是使用丑陋的hack来过度分配,然后留下部分未使用的分配? - Peter Cordes
@PeterCordes 我认为这更多是代码风格而不是性能问题,因为大多数情况下开销(例如511B)将小于1%。当然,只要256对齐是您想要的,您可以简单地使用类似reinterpret_cast<ElementType*>( new __m256i[ nBytesToAllocate / sizeof( __m256i ) ] )的东西。但是,使用虚构结构可能更具可移植性:struct DummyAligned{ alignas( 512 ) char[512] dummy; };。但请注意,如果您的向量大小不是对齐的倍数,这也会导致过度分配... - mxmlnkn
1
@PeterCordes 好的,这完全可以理解。经过进一步的实验和阅读,我改变了我的答案,使用了C++17的对齐new/delete运算符。 - mxmlnkn
我认为分配器现在是正确的,但不幸的是,正如其他答案评论中提到的那样,我不认为保证std::vector实际上会将其元素放置在分配的开头,这可能会破坏对齐方式。(但我不认为任何实现都会以这种方式实现向量。) - user17732522
1
MSVC 在调试构建中不喜欢这个;请参见 https://dev59.com/ZnQOtIcB2Jgan1znlJWy。 - MSalters
显示剩余5条评论

0
标准 C++ 库中的所有容器,包括向量,都具有可选的模板参数 指定容器的分配器,而实现自己的分配器并不需要太多的工作:
class my_awesome_allocator {
};

std::vector<float, my_awesome_allocator> awesomely_allocated_vector;

您需要编写一些代码来实现您的分配器,但它不会比您已经编写的代码多太多。如果您不需要支持 C++17 之前的版本,您只需要实现 allocate()deallocate() 方法即可。


2
这可能是一个很好的地方,可以提供一个规范答案,并附上一个示例,供人们复制/粘贴以跳过C++的烦人障碍。(如果有一种方法让std::vector尝试原地重新分配而不是通常的蠢笨的C++始终alloc + copy,则奖励得分。)当然,还要注意,这个“vector<float,MAA>”与“vector<float>”不兼容(并且不能兼容,因为对普通的“std :: vector <float>”进行“.push_back”编译时没有此分配器可能会进行新的分配并最小限度地复制到对齐内存中。而new / delete与aligned_alloc / free不兼容)。 - Peter Cordes
2
我认为没有任何保证分配器返回的指针直接用作std::vector数组的基地址。例如,我可以想象一个实现std::vector的方式,只使用一个指向分配内存的指针,该指针将end/capacity/allocator存储在值范围之前的内存中。这可能会轻松破坏分配器所做的对齐。 - Dietmar Kühl
2
除了std::vector保证它之外,它就是用来保证的。也许你应该查看一下C++标准在这里的规定。 - Sam Varshavchik
1
他们也不需要专门化allocator_traits-- 不,他们不需要。所需的只是实现一个符合要求的分配器。 - Andrey Semashev
如果有一种方法让std::vector尝试原地重新分配而不是通常的蠢笨的C++总是分配+复制,那将会得到额外的加分。--除非首先保留所需的容量,然后根据需要插入元素,否则没有办法。realloc不调用构造函数,并且对于大多数类型来说,复制原始字节是无效的。此外,realloc的实用性被高估了,因为大多数时候增加分配大小相当于malloc+memcpy+free - Andrey Semashev
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接