koturnの日記

転職したい社会人2年生の技術系日記.ブログ上のコードはコピペ自由です.

std::vectorなどでアラインされた領域を用いる

はじめに

前回の記事でCPU SIMD命令(SSE/AVX/NEON)を紹介した. ただ,前回の記事では静的配列や単純な動的確保のパターンでしかアラインされた領域を用いる手法しか紹介しなかった.

今回は,C++でよく用いられるであろう std::vector でアラインされた領域を用いる手法を紹介する. これにより, std::vector などのSTLのデータにもSIMD処理を適用することが可能となる. また, std::unique_ptrstd::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_ptrstd::shared_ptr のカスタムデリータ

std::unique_ptrstd::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の QScopedPointerQSharedPointer のカスタムデリータ

Qtには std::unique_ptr に相当する QScopedPointerstd::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_ptrstd::shared_ptr でアラインされた領域を管理する場合,カスタムデリータで解放処理を指定する必要がある.

参考文献