読者です 読者をやめる 読者になる 読者になる

koturnの日記

転職したい社会人1年生の技術系日記

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 でもアラインされたメモリを使用できる.

さて, std::vector のアロケータの自作であるが,以下を参考にした. std::allocator の実装が簡略化されて紹介されている.

カスタムアロケータの自作は std::allocator を継承する形で実装する. オーバーライドすべきメンバ関数は,

  • allocate()
  • deallocate()

の2つである. 実装例としては以下のようになる.

// $ g++ -std=gnu++11 main.cpp -o main
#include <cstdlib>
#include <iostream>
#include <type_traits>
#include <vector>


/*!
 * @brief アラインメントされたメモリを動的確保する関数
 * @param [in] size       確保するメモリサイズ (単位はbyte)
 * @param [in] alignment  アラインメント (2のべき乗を指定すること)
 * @return  アラインメントし,動的確保されたメモリ領域へのポインタ
 */
template<typename T = void*, typename std::enable_if<std::is_pointer<T>::value, std::nullptr_t>::type = nullptr>
static inline T
alignedMalloc(std::size_t size, std::size_t alignment) noexcept
{
#if defined(_MSC_VER) || defined(__MINGW32__)
  return reinterpret_cast<T>(_aligned_malloc(size, alignment));
#else
  void* p;
  return reinterpret_cast<T>(posix_memalign(&p, alignment, size) == 0 ? p : nullptr);
#endif  // defined(_MSC_VER) || defined(__MINGW32__)
}


/*!
 * @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 アラインメントされたメモリを確保するSTLのカスタムアロケータ
 */
template<typename T, std::size_t N>
class AligndSTLAllocator :
  public std::allocator<T>
{
public:
  using size_type = typename std::allocator<T>::size_type;
  using pointer = typename std::allocator<T>::pointer;
  using const_pointer = typename std::allocator<T>::const_pointer;

  /*!
   * @brief STLのメモリ領域を動的確保する
   *
   * @param [in] n              確保する要素数
   * @param [in] const_pointer  使用しない引数
   * @return  アラインされたメモリ領域へのポインタ
   */
  pointer
  allocate(size_type n, const_pointer = nullptr) const
  {
    if (n > this->max_size()) {
      throw std::bad_alloc("Cannot allocate aligned memory.");
    }
    return alignedMalloc<pointer>(n * sizeof(T), N);
  }

  /*!
   * @brief STLのメモリ領域を解放する
   * @param [in,out] p  動的確保したメモリ領域
   * @param [in]     n  要素数 (使用しない引数)
   */
  void
  deallocate(pointer p, size_type) const noexcept
  {
    alignedFree(p);
  }
};


/*!
 * @brief このプログラムのエントリポイント
 * @return 終了ステータス
 */
int
main()
{
  static constexpr int ALIGN = 32;

  std::vector<int, AligndSTLAllocator<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 <cstdlib>
#include <iostream>
#include <memory>
#include <type_traits>


/*!
 * @brief アラインメントされたメモリを動的確保する関数
 * @param [in] size       確保するメモリサイズ (単位はbyte)
 * @param [in] alignment  アラインメント (2のべき乗を指定すること)
 * @return  アラインメントし,動的確保されたメモリ領域へのポインタ
 */
template<typename T = void*, typename std::enable_if<std::is_pointer<T>::value, std::nullptr_t>::type = nullptr>
static inline T
alignedMalloc(std::size_t size, std::size_t alignment) noexcept
{
#if defined(_MSC_VER) || defined(__MINGW32__)
  return reinterpret_cast<T>(_aligned_malloc(size, alignment));
#else
  void* p;
  return reinterpret_cast<T>(posix_memalign(&p, alignment, size) == 0 ? p : nullptr);
#endif  // defined(_MSC_VER) || defined(__MINGW32__)
}


/*!
 * @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<unsigned char, AlignedDeleter> up01(alignedMalloc<unsigned char*>(N * sizeof(unsigned char), 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<unsigned char, decltype(&alignedFree)> up02(alignedMalloc<unsigned char*>(N * sizeof(unsigned char), 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<unsigned char, decltype(deleter)> up03(alignedMalloc<unsigned char*>(N * sizeof(unsigned char), 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<unsigned char> sp01(alignedMalloc<unsigned char*>(N * sizeof(unsigned char), 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<unsigned char> sp02(alignedMalloc<unsigned char*>(N * sizeof(unsigned char), 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<unsigned char> sp03(alignedMalloc<unsigned char*>(N * sizeof(unsigned char), 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 アラインメントされたメモリを動的確保する関数
 * @param [in] size       確保するメモリサイズ (単位はbyte)
 * @param [in] alignment  アラインメント (2のべき乗を指定すること)
 * @return  アラインメントし,動的確保されたメモリ領域へのポインタ
 */
template<typename T = void*, typename std::enable_if<std::is_pointer<T>::value, std::nullptr_t>::type = nullptr>
static inline T
alignedMalloc(std::size_t size, std::size_t alignment) noexcept
{
#if defined(_MSC_VER) || defined(__MINGW32__)
  return reinterpret_cast<T>(_aligned_malloc(size, alignment));
#else
  void* p;
  return reinterpret_cast<T>(posix_memalign(&p, alignment, size) == 0 ? p : nullptr);
#endif  // defined(_MSC_VER) || defined(__MINGW32__)
}


/*!
 * @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(alignedMalloc<quint8*>(N * sizeof(quint8), ALIGN));
  if (scopedPtr.data() == nullptr) {
    qDebug() << "Failed to allocate memory.";
    return 1;
  }
  checkAlignment("scopedPtr", scopedPtr.data(), ALIGN);

  QScopedArrayPointer<quint8, AlignedQDeleter> scopedArrayPtr(alignedMalloc<quint8*>(N * sizeof(quint8), ALIGN));
  if (scopedArrayPtr.data() == nullptr) {
    qDebug() << "Failed to allocate memory.";
    return 1;
  }
  checkAlignment("scopedArrayPtr", scopedArrayPtr.data(), ALIGN);

  QSharedPointer<quint8> sharedPtr(alignedMalloc<quint8*>(N * sizeof(quint8), 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 でアラインされた領域を管理する場合,カスタムデリータで解放処理を指定する必要がある.

参考文献