先日(2017年11月30日の日記参照)CPUによるモナコインというかLyra2REv2の計算で、ボトルネックとなっていたCubeHashをSSE化してみました。今回はARMでチャレンジしてみます。
Raspberry Pi 3(ARM Cortex A53/1.2GHz x 4)でCPUマイナーを実行してみるとたったの8kH/sしか出ません。4コア並列で動作させると32kH/sとなり、きっちり4倍になるのは素晴らしい(※)ですが、x86_64 CPUの1コアにも敵わないです。
NEONにもIntrinsicsがあることを知ったので、不親切なNEON命令のマニュアルと戦いながら、CubeHashをNEON化してみたところ、10kH/sほどになりました。
#if defined(__ARM_NEON__)
# include <arm_neon.h>
#endif
//...
#define NEON_ROTL(x, n) do { \
uint32x4_t mw0, mw1; \
mw0 = vshlq_n_u32((x), (n)); \
mw1 = vshrq_n_u32((x), 32 - (n)); \
x = vorrq_u32(mw0, mw1); \
} while (0);
#define NEON_SWP(a, b) do { \
uint32x4_t mw; \
mw = b; \
b = a; \
a = mw; \
} while (0);
#define NEON_STEP5(x) do { \
uint64x2_t mw; \
mw = vreinterpretq_u64_u32((x)); \
mw = vextq_u64(mw, mw, 1); \
x = vreinterpretq_u32_u64(mw); \
} while (0);
#define ROUND_ONE_NEON do { \
mxg = vaddq_u32(mx0, mxg); \
mxk = vaddq_u32(mx4, mxk); \
mxo = vaddq_u32(mx8, mxo); \
mxs = vaddq_u32(mxc, mxs); \
NEON_ROTL(mx0, 7); \
NEON_ROTL(mx4, 7); \
NEON_ROTL(mx8, 7); \
NEON_ROTL(mxc, 7); \
NEON_SWP(mx0, mx8); \
NEON_SWP(mx4, mxc); \
mx0 = veorq_u32(mx0, mxg); \
mx4 = veorq_u32(mx4, mxk); \
mx8 = veorq_u32(mx8, mxo); \
mxc = veorq_u32(mxc, mxs); \
NEON_STEP5(mxg); \
NEON_STEP5(mxk); \
NEON_STEP5(mxo); \
NEON_STEP5(mxs); \
mxg = vaddq_u32(mx0, mxg); \
mxk = vaddq_u32(mx4, mxk); \
mxo = vaddq_u32(mx8, mxo); \
mxs = vaddq_u32(mxc, mxs); \
NEON_ROTL(mx0, 11); \
NEON_ROTL(mx4, 11); \
NEON_ROTL(mx8, 11); \
NEON_ROTL(mxc, 11); \
NEON_SWP(mx0, mx4); \
NEON_SWP(mx8, mxc); \
mx0 = veorq_u32(mx0, mxg); \
mx4 = veorq_u32(mx4, mxk); \
mx8 = veorq_u32(mx8, mxo); \
mxc = veorq_u32(mxc, mxs); \
mxg = vrev64q_u32(mxg); \
mxk = vrev64q_u32(mxk); \
mxo = vrev64q_u32(mxo); \
mxs = vrev64q_u32(mxs); \
} while (0)
#define SIXTEEN_ROUNDS_NEON do { \
int j; \
uint32x4_t mx0, mx4, mx8, mxc; \
uint32x4_t mxg, mxk, mxo, mxs; \
mx0 = vld1q_u32((void *)&x0); \
mx4 = vld1q_u32((void *)&x4); \
mx8 = vld1q_u32((void *)&x8); \
mxc = vld1q_u32((void *)&xc); \
mxg = vld1q_u32((void *)&xg); \
mxk = vld1q_u32((void *)&xk); \
mxo = vld1q_u32((void *)&xo); \
mxs = vld1q_u32((void *)&xs); \
for (j = 0; j < 16; j ++) { \
ROUND_ONE_NEON; \
} \
vst1q_u32(&x0, mx0); \
vst1q_u32(&x4, mx4); \
vst1q_u32(&x8, mx8); \
vst1q_u32(&xc, mxc); \
vst1q_u32(&xg, mxg); \
vst1q_u32(&xk, mxk); \
vst1q_u32(&xo, mxo); \
vst1q_u32(&xs, mxs); \
} while (0)
//...
#if defined(__ARM_NEON__)
# define ROUND_ONE ROUND_ONE_NEON
# define SIXTEEN_ROUNDS SIXTEEN_ROUNDS_NEON
#else
# define ROUND_ONE ROUND_ONE_SLOW
# define SIXTEEN_ROUNDS SIXTEEN_ROUNDS_SLOW
#endif
前回と同様にcpuminer-multiのマクロに無理矢理はめ込んで実装しています。NEONを触るのは初めてで、非効率的な書き方になっているかもしれません。お気づきの点があれば教えてくださいませ。
(※)AMD A10-7600は昨日書いた通り1コア145kH/sですが、4コア並列だと145 x 4 = 580kH/sとはならず、少し効率が落ち490〜500kH/sほどになります。
前回SSE化したときは1ラウンドの処理だけ書き換えれば事足りましたが、今回NEON化したときは16ラウンドのループも書き換える必要がありました。
何故かというとx64と違ってarmhfの場合、コンパイラがあまり良い結果を出力してくれないからです。gcc-7.2 x64の場合、
このような処理をループさせても、生成されたバイナリの逆アセンブルを見ると、
以上のようにload/storeの無駄を検知してループ「外」に追い出してくれました。しかしgcc-4.9 armhfの場合、ループ「内」にload/storeが残ってしまい、かなり遅くなります。
原因としてgccのバージョンが古い、アーキテクチャの最適化がこなれてない、NEONのIntrinsicsを使うと最適化が制限される、などいくつか考えられますが、今のところ分かりません。gcc-7にしたらコンパイラが賢くやってくれるようになれば一番楽ですけどね……。
< | 2017 | > | ||||
<< | < | 12 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | - | - | - | - | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | - | - | - | - | - | - |
合計:
本日:
管理者: Katsuhiro Suzuki(katsuhiro( a t )katsuster.net)
This is Simple Diary 1.0
Copyright(C) Katsuhiro Suzuki 2006-2023.
Powered by PHP 8.2.15.
using GD bundled (2.1.0 compatible)(png support.)