はじめに
前回の記事でCPU SIMD命令(SSE/AVX/NEON)を紹介した. ただ,前回の記事では静的配列や単純な動的確保のパターンでしかアラインされた領域を用いる手法しか紹介しなかった.
今回は,C++でよく用いられるであろう std::vector
でアラインされた領域を用いる手法を紹介する.
これにより, std::vector
などのSTLのデータにもSIMD処理を適用することが可能となる.
また, std::unique_ptr
, std::shared_ptr
でアラインされた領域を解放する手法も紹介する.
STLのカスタムアロケータ
std::vector
などのSTLメモリ確保は基本的に ::operator new
を用いて行われる.
これのメモリ確保は std::allocator
というクラスが担当し, STL内部で用いられている.
例えば, std::vector
のテンプレートの第二引数を省略した場合, std::allocator
が用いられる.
この std::vector
のテンプレート第二引数に自作アロケータを指定すると, std::vector
でもアラインされたメモリを使用できる.
さて,アロケータの自作であるが,C++11より前であれば,std::allocator
を継承し,最小限のメンバ関数 allocate()
と deallocate()
を実装することで完成させることができた.
C++11以降では,std::allocator_traits
に足りない実装を任せることが推奨となっており,C++17以降では std::allocator
の継承は非推奨となっている.
アロケータの実装は以下を参考にした.
実装例としては以下のようになる.
// $ g++ -std=gnu++11 main.cpp -o main #include <cstdlib> #include <iostream> #include <type_traits> #include <vector> #if defined(_MSC_VER) || defined(__MINGW32__) # include <malloc.h> #endif // defined(_MSC_VER) || defined(__MINGW32__) /*! * @brief アラインメントされたメモリを動的確保する関数 * @tparam T 確保するメモリの要素型.この関数の返却値はT* * @param [in] nBytes 確保するメモリサイズ (単位はbyte) * @param [in] alignment アラインメント (2のべき乗を指定すること) * @return アラインメントし,動的確保されたメモリ領域へのポインタ */ template<typename T = void> static inline T* alignedMalloc(std::size_t nBytes, std::size_t alignment = alignof(T)) noexcept { #if defined(_MSC_VER) || defined(__MINGW32__) return reinterpret_cast<T*>(::_aligned_malloc(nBytes, alignment)); #else void* p; return reinterpret_cast<T*>(::posix_memalign(&p, alignment, nBytes) == 0 ? p : nullptr); #endif // defined(_MSC_VER) || defined(__MINGW32__) } /*! * @brief アラインメントされたメモリを動的確保する関数.配列向けにalignedMallocの引数指定が簡略化されている * @tparam T 確保する配列の要素型.この関数の返却値はT* * @param [in] size 確保する要素数.すなわち確保するサイズは size * sizeof(T) * @param [in] alignment アラインメント (2のべき乗を指定すること) * @return アラインメントし,動的確保されたメモリ領域へのポインタ */ template<typename T> static inline T* alignedAllocArray(std::size_t size, std::size_t alignment = alignof(T)) noexcept { return alignedMalloc<T>(size * sizeof(T), alignment); } /*! * @brief アラインメントされたメモリを解放する関数 * @param [in] ptr 解放対象のメモリの先頭番地を指すポインタ */ static inline void alignedFree(void* ptr) noexcept { #if defined(_MSC_VER) || defined(__MINGW32__) ::_aligned_free(ptr); #else std::free(ptr); #endif // defined(_MSC_VER) || defined(__MINGW32__) } /*! * @brief アラインされたメモリ領域を確保するアロケータ */ template< typename T, std::size_t kAlignment = alignof(T) > class AlignedAllocator { public: //! 要素型 using value_type = T; //! サイズ型 using size_type = std::size_t; //! 要素のポインタ型 using pointer = typename std::add_pointer<value_type>::type; //! 要素の読み取り専用ポインタ型 using const_pointer = typename std::add_pointer<const value_type>::type; /*! * @brief 再束縛のための別の要素型のアロケータの定義 * * AlignedAllocatorが2つのテンプレート引数を持つため,この定義は必須 */ template<class U> struct rebind { //! 再束縛のためのアロケータ型 using other = AlignedAllocator<U, kAlignment>; }; /*! * デフォルトコンストラクタ */ AlignedAllocator() noexcept {} /*! * 別の要素型を受け取るコンストラクタ */ template<typename U> AlignedAllocator(const AlignedAllocator<U, kAlignment>&) noexcept {} /*! * @brief メモリ領域を動的確保する * * @param [in] n 確保する要素数 * @param [in] hint ヒント(使用しない) * @return アラインされたメモリ領域へのポインタ */ pointer allocate(size_type n, const_pointer /* hint */ = nullptr) const { auto p = alignedAllocArray<value_type>(n, kAlignment); if (p == nullptr) { throw std::bad_alloc{}; } return p; } /*! * @brief メモリ領域を解放する * @param [in,out] p 動的確保したメモリ領域 * @param [in] n 要素数 (使用しない引数) */ void deallocate(pointer p, size_type /* n */) const noexcept { alignedFree(p); } }; // class AlignedAllocator template< typename T, std::size_t kAlignment1, typename U, std::size_t kAlignment2 > static inline bool operator==(const AlignedAllocator<T, kAlignment1>&, const AlignedAllocator<U, kAlignment2>&) noexcept { return kAlignment1 == kAlignment2; } template< typename T, std::size_t kAlignment1, typename U, std::size_t kAlignment2 > static inline bool operator!=(const AlignedAllocator<T, kAlignment1>& lhs, const AlignedAllocator<U, kAlignment2>& rhs) noexcept { return !(lhs == rhs); } /*! * @brief このプログラムのエントリポイント * @return 終了ステータス */ int main() { static constexpr int ALIGN = 32; std::vector<int, AlignedAllocator<int, ALIGN> > vec(32); if ((reinterpret_cast<std::ptrdiff_t>(vec.data())) % ALIGN == 0) { std::cout << "vector memory is " << ALIGN << " byte aligned." << std::endl; } else { std::cout << "vector memory is not " << ALIGN << " byte aligned." << std::endl; } return EXIT_SUCCESS; }
std::unique_ptr
と std::shared_ptr
のカスタムデリータ
std::unique_ptr
と std::shared_ptr
はポインタを管理するクラスであり,自動的に始末処理を行ってくれる.
始末処理はユーザが指定することが可能である.
指定の方法として,
- 関数オブジェクト
- 関数ポインタ
- ラムダ
を用いるものの3つがある. これらの実装例は以下のようになる.
// $ g++ -std=gnu++11 main.cpp -o main #include <cstdint> #include <cstdlib> #include <iostream> #include <memory> #include <type_traits> #if defined(_MSC_VER) || defined(__MINGW32__) # include <malloc.h> #endif /*! * @brief アラインメントされたメモリを動的確保する関数 * @tparam T 確保するメモリの要素型.この関数の返却値はT* * @param [in] nBytes 確保するメモリサイズ (単位はbyte) * @param [in] alignment アラインメント (2のべき乗を指定すること) * @return アラインメントし,動的確保されたメモリ領域へのポインタ */ template<typename T = void> static inline T* alignedMalloc(std::size_t nBytes, std::size_t alignment = alignof(T)) noexcept { #if defined(_MSC_VER) || defined(__MINGW32__) return reinterpret_cast<T*>(::_aligned_malloc(nBytes, alignment)); #else void* p; return reinterpret_cast<T*>(::posix_memalign(&p, alignment, nBytes) == 0 ? p : nullptr); #endif // defined(_MSC_VER) || defined(__MINGW32__) } /*! * @brief アラインメントされたメモリを動的確保する関数.配列向けにalignedMallocの引数指定が簡略化されている * @tparam T 確保する配列の要素型.この関数の返却値はT* * @param [in] size 確保する要素数.すなわち確保するサイズは size * sizeof(T) * @param [in] alignment アラインメント (2のべき乗を指定すること) * @return アラインメントし,動的確保されたメモリ領域へのポインタ */ template<typename T> static inline T* alignedAllocArray(std::size_t size, std::size_t alignment = alignof(T)) noexcept { return alignedMalloc<T>(size * sizeof(T), alignment); } /*! * @brief アラインメントされたメモリを解放する関数 * @param [in] ptr 解放対象のメモリの先頭番地を指すポインタ */ static inline void alignedFree(void* ptr) noexcept { #if defined(_MSC_VER) || defined(__MINGW32__) ::_aligned_free(ptr); #else std::free(ptr); #endif // defined(_MSC_VER) || defined(__MINGW32__) } /*! * @brief std::unique_ptr で利用するアラインされたメモリ用のカスタムデリータ */ struct AlignedDeleter { /*! * @brief デリート処理を行うオペレータ * @param [in,out] p アラインメントされたメモリ領域へのポインタ */ void operator()(void* p) const noexcept { alignedFree(p); } }; /*! * @brief アラインメントをチェックする * @param [in] name チェック対象ポインタ名 * @param [in] ptr チェック対象ポインタ * @param [in] alignment 期待するアラインメント */ static inline void checkAlignment(const std::string& name, const void* ptr, std::size_t alignment) noexcept { if ((reinterpret_cast<std::ptrdiff_t>(ptr)) % alignment == 0) { std::cout << name << " is " << alignment << " byte aligned." << std::endl; } else { std::cout << name << " is not " << alignment << " byte aligned." << std::endl; } } /*! * @brief このプログラムのエントリポイント * @return 終了ステータス */ int main() { static constexpr int ALIGN = 32; static constexpr int N = 10; // std::unique_ptr にカスタムデリータを指定する例1: 関数オブジェクト std::unique_ptr<std::uint8_t, AlignedDeleter> up01(alignedAllocArray<std::uint8_t>(N, ALIGN)); if (up01.get() == nullptr) { std::cerr << "Failed to allocate memory." << std::endl; return 1; } checkAlignment("up01", up01.get(), ALIGN); // std::unique_ptr にカスタムデリータを指定する例2: 関数ポインタ std::unique_ptr<std::uint8_t, decltype(&alignedFree)> up02(alignedAllocArray<std::uint8_t>(N, ALIGN), alignedFree); if (up02.get() == nullptr) { std::cerr << "Failed to allocate memory." << std::endl; return 1; } checkAlignment("up02", up02.get(), ALIGN); // std::unique_ptr にカスタムデリータを指定する例3: ラムダ auto deleter = [](void* ptr){ alignedFree(ptr); }; std::unique_ptr<std::uint8_t, decltype(deleter)> up03(alignedAllocArray<std::uint8_t>(N, ALIGN), deleter); if (up03.get() == nullptr) { std::cerr << "Failed to allocate memory." << std::endl; return 1; } checkAlignment("up03", up03.get(), ALIGN); // std::shared_ptr にカスタムデリータを指定する例1: 関数オブジェクト std::shared_ptr<std::uint8_t> sp01(alignedAllocArray<std::uint8_t>(N, ALIGN), AlignedDeleter()); if (sp01.get() == nullptr) { std::cerr << "Failed to allocate memory." << std::endl; return 1; } checkAlignment("sp01", sp01.get(), ALIGN); // std::shared_ptr にカスタムデリータを指定する例2: 関数ポインタ std::shared_ptr<std::uint8_t> sp02(alignedAllocArray<std::uint8_t>(N, ALIGN), alignedFree); if (sp02.get() == nullptr) { std::cerr << "Failed to allocate memory." << std::endl; return 1; } checkAlignment("sp02", sp02.get(), ALIGN); // std::shared_ptr にカスタムデリータを指定する例3: ラムダ std::shared_ptr<std::uint8_t> sp03(alignedAllocArray<std::uint8_t>(N, ALIGN), [](void* ptr) { alignedFree(ptr); }); if (sp03.get() == nullptr) { std::cerr << "Failed to allocate memory." << std::endl; return 1; } checkAlignment("sp03", sp03.get(), ALIGN); return 0; }
おまけ: Qtの QScopedPointer
, QSharedPointer
のカスタムデリータ
Qtには std::unique_ptr
に相当する QScopedPointer
と std::shared_ptr
に相当する QSharedPointer
がある.
これらのカスタムデリータについてもおまけで紹介する.
#include <QCoreApplication> #include <QDebug> #include <QScopedPointer> #include <QScopedArrayPointer> #include <QSharedPointer> #include <string> /*! * @brief アラインメントされたメモリを動的確保する関数 * @tparam T 確保するメモリの要素型.この関数の返却値はT* * @param [in] nBytes 確保するメモリサイズ (単位はbyte) * @param [in] alignment アラインメント (2のべき乗を指定すること) * @return アラインメントし,動的確保されたメモリ領域へのポインタ */ template<typename T = void> static inline T* alignedMalloc(std::size_t nBytes, std::size_t alignment = alignof(T)) noexcept { #if defined(_MSC_VER) || defined(__MINGW32__) return reinterpret_cast<T*>(::_aligned_malloc(nBytes, alignment)); #else void* p; return reinterpret_cast<T*>(::posix_memalign(&p, alignment, nBytes) == 0 ? p : nullptr); #endif // defined(_MSC_VER) || defined(__MINGW32__) } /*! * @brief アラインメントされたメモリを動的確保する関数.配列向けにalignedMallocの引数指定が簡略化されている * @tparam T 確保する配列の要素型.この関数の返却値はT* * @param [in] size 確保する要素数.すなわち確保するサイズは size * sizeof(T) * @param [in] alignment アラインメント (2のべき乗を指定すること) * @return アラインメントし,動的確保されたメモリ領域へのポインタ */ template<typename T> static inline T* alignedAllocArray(std::size_t size, std::size_t alignment = alignof(T)) noexcept { return alignedMalloc<T>(size * sizeof(T), alignment); } /*! * @brief アラインメントされたメモリを解放する関数 * @param [in] ptr 解放対象のメモリの先頭番地を指すポインタ */ static inline void alignedFree(void* ptr) noexcept { #if defined(_MSC_VER) || defined(__MINGW32__) ::_aligned_free(ptr); #else std::free(ptr); #endif // defined(_MSC_VER) || defined(__MINGW32__) } /*! * @brief QScopedPointer, QScopedArrayPointer, QSharedPointer の * カスタムデリータを提供する */ struct AlignedQDeleter { /*! * @brief QScopedPointer, QScopedArrayPointer で利用するデリート関数 * @param [in,out] ptr アラインメントされたメモリ領域へのポインタ */ static inline void cleanup(void* ptr) noexcept { alignedFree(ptr); } /*! * @brief QSharedPointer で利用するデリート関数 * @param [in,out] ptr アラインメントされたメモリ領域へのポインタ */ inline void operator()(void* ptr) const noexcept { cleanup(ptr); } }; /*! * @brief アラインメントをチェックする * @param [in] name チェック対象ポインタ名 * @param [in] ptr チェック対象ポインタ * @param [in] alignment 期待するアラインメント */ static inline void checkAlignment(const std::string& name, const void* ptr, std::size_t alignment) noexcept { if ((reinterpret_cast<std::ptrdiff_t>(ptr)) % alignment == 0) { qDebug() << name.c_str() << "is" << alignment << "byte aligned."; } else { qDebug() << name.c_str() << "is not" << alignment << "byte aligned."; } } /*! * @brief このプログラムのエントリポイント * @return 終了ステータス */ int main(int argc, char *argv[]) { static constexpr int ALIGN = 32; static constexpr int N = 10; QCoreApplication a(argc, argv); QScopedPointer<quint8, AlignedQDeleter> scopedPtr(alignedAllocArray<quint8>(N, ALIGN)); if (scopedPtr.data() == nullptr) { qDebug() << "Failed to allocate memory."; return 1; } checkAlignment("scopedPtr", scopedPtr.data(), ALIGN); QScopedArrayPointer<quint8, AlignedQDeleter> scopedArrayPtr(alignedAllocArray<quint8>(N, ALIGN)); if (scopedArrayPtr.data() == nullptr) { qDebug() << "Failed to allocate memory."; return 1; } checkAlignment("scopedArrayPtr", scopedArrayPtr.data(), ALIGN); QSharedPointer<quint8> sharedPtr(alignedAllocArray<quint8>(N, ALIGN), AlignedQDeleter()); if (sharedPtr.data() == nullptr) { qDebug() << "Failed to allocate memory."; return 1; } checkAlignment("sharedPtr", sharedPtr.data(), ALIGN); return a.exec(); }
コンパイルするには,以下のようなプロジェクトファイルを準備する.
QT += core QT -= gui CONFIG += c++11 TARGET = CustomDeleterTest CONFIG += console CONFIG -= app_bundle TEMPLATE = app SOURCES += main.cpp
そして,以下の手順でコンパイルを行うとよい.
$ qmake $ make
まとめ
std::vector
にカスタムアロケータを指定することで,データのアラインメントを指定できる.
これにより, std::vector
SIMD処理に用いることが可能となる.
また. std::unique_ptr
, std::shared_ptr
でアラインされた領域を管理する場合,カスタムデリータで解放処理を指定する必要がある.