Paralell force - パラレル・フォースのレンダラの画素アクセスは下記の方針で対応しています。
- 可能な限り XMM レジスタを使用する
- 可能な限り movdqa 命令でロードおよびストアする
前者はともかくとして、後者がなかなか曲者なのです。
movdqa の使用条件は『ストア先またはロード元のアドレスが 16 バイト アライメントされていること』です。レンダラを作ったことがある人は分かると思うのですが、たとえばイメージのアフィン変換などで転送元と転送先の双方の画素を 16 バイト アライメントさせるというのはなかなか至難の業です。16 バイト アライメントされないアドレスへのアクセスには、ちょっと低速な movdqu という命令を使用することになります。
そこで、Parallel force - パラレル・フォースでは、転送先の画素を XMM レジスタ使用してストアする場合についてのみ、16 バイト アライメントを保証するようにしています。まぁ、処理の都合上、ロードとストアの双方をアライメントするのが大変だったので、ストアだけでもアライメントしようとした次第です。
これについて、効果を計測していなかったので、今回計測してみました。Core i7 で、L2 キャッシュに収まるサイズの単純なメモリ コピー ルーチンです。memcpy とあわせて 5 ケース計測しました。
まずはロードとストアがアライメントされるケース。
void xmmcpy_da_sa(void* oDst, const void* iSrc, const std::size_t iLength) { assert(oDst != nullptr); assert(iSrc != nullptr); assert((iLength & 15) == 0); auto aDst = reinterpret_cast<__m128i*>( (reinterpret_cast<std::size_t>(oDst) + 15) & ~15); auto aDstE = aDst + iLength / sizeof(__m128i); auto aDst4 = aDst + (aDstE - aDst) / 4; auto aSrc = reinterpret_cast<const __m128i*>( (reinterpret_cast<std::size_t>(iSrc) + 15) & ~15); while (aDst != aDst4) { const auto aSrc0 = _mm_load_si128(aSrc + 0); const auto aSrc1 = _mm_load_si128(aSrc + 1); const auto aSrc2 = _mm_load_si128(aSrc + 2); const auto aSrc3 = _mm_load_si128(aSrc + 3); _mm_store_si128(aDst + 0, aSrc0); _mm_store_si128(aDst + 1, aSrc1); _mm_store_si128(aDst + 2, aSrc2); _mm_store_si128(aDst + 3, aSrc3); aDst += 4; aSrc += 4; } while (aDst != aDstE) { _mm_store_si128(aDst++, _mm_load_si128(aSrc++)); } }
次いで、ストアのみがアライメントされるケース。
void xmmcpy_da_s(void* oDst, const void* iSrc, const std::size_t iLength) { assert(oDst != nullptr); assert(iSrc != nullptr); assert((iLength & 15) == 0); auto aDst = reinterpret_cast<__m128i*>( (reinterpret_cast<std::size_t>(oDst) + 15) & ~15); auto aDstE = aDst + iLength / sizeof(__m128i); auto aDst4 = aDst + (aDstE - aDst) / 4; auto aSrc = reinterpret_cast<const __m128i*>( ((reinterpret_cast<std::size_t>(iSrc) + 15) & ~15) + 1); // ずらす while (aDst != aDst4) { const auto aSrc0 = _mm_loadu_si128(aSrc + 0); const auto aSrc1 = _mm_loadu_si128(aSrc + 1); const auto aSrc2 = _mm_loadu_si128(aSrc + 2); const auto aSrc3 = _mm_loadu_si128(aSrc + 3); _mm_store_si128(aDst + 0, aSrc0); _mm_store_si128(aDst + 1, aSrc1); _mm_store_si128(aDst + 2, aSrc2); _mm_store_si128(aDst + 3, aSrc3); aDst += 4; aSrc += 4; } while (aDst != aDstE) { _mm_store_si128(aDst++, _mm_loadu_si128(aSrc++)); } }
また、ロードのみがアライメントされるケース。
void xmmcpy_d_sa(void* oDst, const void* iSrc, const std::size_t iLength) { assert(oDst != nullptr); assert(iSrc != nullptr); assert((iLength & 15) == 0); auto aDst = reinterpret_cast<__m128i*>( ((reinterpret_cast<std::size_t>(oDst) + 15) & ~15) + 1); // ずらす auto aDstE = aDst + iLength / sizeof(__m128i); auto aDst4 = aDst + (aDstE - aDst) / 4; auto aSrc = reinterpret_cast<const __m128i*>( (reinterpret_cast<std::size_t>(iSrc) + 15) & ~15); while (aDst != aDst4) { const auto aSrc0 = _mm_load_si128(aSrc + 0); const auto aSrc1 = _mm_load_si128(aSrc + 1); const auto aSrc2 = _mm_load_si128(aSrc + 2); const auto aSrc3 = _mm_load_si128(aSrc + 3); _mm_storeu_si128(aDst + 0, aSrc0); _mm_storeu_si128(aDst + 1, aSrc1); _mm_storeu_si128(aDst + 2, aSrc2); _mm_storeu_si128(aDst + 3, aSrc3); aDst += 4; aSrc += 4; } while (aDst != aDstE) { _mm_storeu_si128(aDst++, _mm_load_si128(aSrc++)); } }
最後に、ロードもストアもアライメントされないケース。
void xmmcpy_d_s(void* oDst, const void* iSrc, const std::size_t iLength) { assert(oDst != nullptr); assert(iSrc != nullptr); assert((iLength & 15) == 0); auto aDst = reinterpret_cast<__m128i*>( ((reinterpret_cast<std::size_t>(oDst) + 15) & ~15) + 1); // ずらす auto aDstE = aDst + iLength / sizeof(__m128i); auto aDst4 = aDst + (aDstE - aDst) / 4; auto aSrc = reinterpret_cast<const __m128i*>( ((reinterpret_cast<std::size_t>(iSrc) + 15) & ~15) + 1); // ずらす while (aDst != aDst4) { const auto aSrc0 = _mm_loadu_si128(aSrc + 0); const auto aSrc1 = _mm_loadu_si128(aSrc + 1); const auto aSrc2 = _mm_loadu_si128(aSrc + 2); const auto aSrc3 = _mm_loadu_si128(aSrc + 3); _mm_storeu_si128(aDst + 0, aSrc0); _mm_storeu_si128(aDst + 1, aSrc1); _mm_storeu_si128(aDst + 2, aSrc2); _mm_storeu_si128(aDst + 3, aSrc3); aDst += 4; aSrc += 4; } while (aDst != aDstE) { _mm_storeu_si128(aDst++, _mm_loadu_si128(aSrc++)); } }
これらを下記のように計測してみました。
#include <emmintrin.h> #include <cstdint> #include <cstring> #include <cassert> #include <ctime> #include <iostream> const std::size_t gLength = 64 * 1024; std::uint8_t gDst[gLength + 16 + 1]; std::uint8_t gSrc[gLength + 16 + 1]; template <class Func> void calc(const char* iPrefix, Func iFunc) { const auto aPrev = std::clock(); for (std::uint_fast32_t aCt = 0; aCt < 1000000 * 2; aCt++) { iFunc(gDst, gSrc, gLength); } const auto aCurr = std::clock(); std::cout << iPrefix << (static_cast<double>(aCurr - aPrev) / CLOCKS_PER_SEC) << std::endl; } int main() { calc("prepare : ", std::memcpy); calc("d/s align : ", xmmcpy_da_sa); calc("dst align : ", xmmcpy_da_s); calc("src align : ", xmmcpy_d_sa); calc("not align : ", xmmcpy_d_s); calc("normal cpy: ", std::memcpy); return 0; }
結果はこちら。
prepare : 13.572 d/s align : 6.723 dst align : 6.911 src align : 6.724 not align : 7.051 normal cpy: 13.541
std::memcpy が遅いのはともかくとして…。XMM レジスタを使用したケースでは、なんと。ロードとストアのいずれかをアライメントするのであればロードの方が有利という結果に。まぁ、Parallel force - パラレル・フォースで採用した、ストアだけをアライメントするケースでも、非アライメントのケースよりは速いので良しとします。計測してみると、いろいろと傾向が分かるものですね。