Instantly share code, notes, and snippets.

@t-nissie /00pow48.md
Last active Aug 18, 2016

Embed
What would you like to do?
GCC-6でpow(x, 1/32.), pow(x, 1/48.), pow(x, 1/72.)のベンチマーク

pow(x, 1/32.), pow(x, 1/48.), pow(x, 1/72.)のベンチマーク

はじめに

GCC-6からpow(x, 1/32.)などがsqrt()を使って最適化されるようになった[1]。 pow(x, 1/32.), pow(x, 1/48.), pow(x, 1/72.)のベンチマークを行う[2]。 C++11を使ってみた。

sqrt()はハードウエアがあるが、使われているかどうかは不明。cbrt()のハードウエアは普通のCPUにはない[3,4]。

この文章の最新版はGistに置いてある。

ベンチマークコード

pow48.cppがベンチマークコード。 Makefileを用意した。 make clean; makeでGNU C++ (g++)とIntel C++ (icpc)とを試すことができる。

pow48.cppで計時は C++11のstd::chronoを使っている[5]。

Results

g++の新旧バージョンが両方とも入っているマシンを持っていない。 ライセンスが高価なicpcは少数のマシンにしかインストールされていない。

Intel Xeon E5-2680 v3 (g++ version 4.8.3)

g++ version 4.8.3ではまだpow(x, 1/32.)の最適化ができない。

$ grep Xeon /proc/cpuinfo | head -1
model name: Intel(R) Xeon(R) CPU E5-2680 v3 @ 2.50GHz
$ make clean
rm -f *.o core *.s *.ps pow48_g++ pow48_icpc 00pow48.html
$ make
g++ --version | head -1
g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-4)
g++  -std=c++11 -Ofast pow48.cpp -o pow48_g++
./pow48_g++
32,pow          :  3870 ms, 1.93965617861778
32,sqrt*5       :   720 ms, 1.93965617861778
48,pow          :  3737 ms, 1.555308347057671
48,cbrt,sqrt*4  :  3917 ms, 1.555308347057671
72,pow          :  3741 ms, 1.342386786199955
72,cbrt*2,sqrt*4:  5772 ms, 1.342386786199955

Intel Celeron 1037U 1.80GHz (g++ version 4.8.4)

Ubuntu 14.04.4 LTSのg++ 4.8.4-2ubuntu1~14.04.1でもpow(x, 1/32.)の最適化ができない。

$ grep Celeron /proc/cpuinfo | head -1
model name: Intel(R) Celeron(R) CPU 1037U @ 1.80GHz
$ make clean
rm -f *.o core *.s *.ps *.ps~ pow48_g++ pow48_icpc pow48f_gfortran 00pow48.html
$ make
g++ --version | head -1
g++ (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4
g++  -std=c++11 -Ofast pow48.cpp -o pow48_g++
./pow48_g++
32,pow          :  7597 ms, 1.93965617861778
32,sqrt*5       :  1327 ms, 1.93965617861778
48,pow          :  7241 ms, 1.555308347057671
48,cbrt,sqrt*4  :  7381 ms, 1.555308347057671
72,pow          :  7243 ms, 1.342386786199955
72,cbrt*2,sqrt*4: 10916 ms, 1.342386786199955

Intel Xeon X5650 (g++ version 6.0.0)

すこし古いXeon。 GNU C++ (g++)でpow(x, 1/32.)が最適化されている様子がわかる。 Intel C++ (icpc)がさらに速い実行形式を作ってくれる。 icpcのアセンブラ出力を見るとsvmlってたくさん出でくる[6]。 SIMD命令がうまく使われて速くなっていたりするのかも。

$ grep Xeon /proc/cpuinfo | head -1
model name: Intel(R) Xeon(R) CPU           X5650  @ 2.67GHz
$ make clean
rm -f *.o core *.s *.ps pow48_g++ pow48_icpc 00pow48.html
$ make
g++ --version | head -1
g++ (GCC) 6.0.0 20160306 (experimental)
g++  -std=c++11 -Ofast pow48.cpp -o pow48_g++
./pow48_g++
32,pow          :  2019 ms, 1.93965617861778
32,sqrt*5       :  1884 ms, 1.93965617861778
48,pow          :  5366 ms, 1.555308347057671
48,cbrt,sqrt*4  :  5406 ms, 1.555308347057671
72,pow          :  5516 ms, 1.342386786199955
72,cbrt*2,sqrt*4:  5571 ms, 1.342386786199955
icpc -V |& head -1
Intel(R) C++ Intel(R) 64 Compiler XE for applications running on Intel(R) 64, Version 15.0.3.187 Build 20150407
icpc -std=c++11 -O3    pow48.cpp -o pow48_icpc
./pow48_icpc
32,pow          :  1505 ms, 1.93965617861778
32,sqrt*5       :  1755 ms, 1.93965617861778
48,pow          :  1359 ms, 1.555308347057671
48,cbrt,sqrt*4  :  1746 ms, 1.555308347057671
72,pow          :  1358 ms, 1.342386786199955
72,cbrt*2,sqrt*4:  2079 ms, 1.342386786199955
$ icpc -std=c++11 -O3 pow48.cpp -S
$ grep svml pow48.s
                    :
        call      __svml_pow2                                   #44.38
        call      __svml_cbrt2                                  #49.38
                    :

Intel Core i5 (Ivy Bridge) 2.5 GHz on MacBook Pro (Retina, 13-inch, Late 2012):

このマシンだけ生成される擬似乱数列が異なる。Mac OS Xのライブラリの関係か???

$ system_profiler | grep 'Processor '
      Processor Name: Intel Core i5
      Processor Speed: 2.5 GHz
$ make clean
rm -f *.o core *.s *.ps pow48_g++ pow48_icpc 00pow48.html
$ make
g++ --version | head -1
g++ (GCC) 6.0.0 20160124 (experimental)
g++  -std=c++11 -Ofast pow48.cpp -o pow48_g++
./pow48_g++
32,pow          :   796 ms, 1.919878048947668
32,sqrt*5       :   784 ms, 1.919878048947668
48,pow          :  2321 ms, 1.544717602206269
48,cbrt,sqrt*4  :  2342 ms, 1.544717602206269
72,pow          :  2327 ms, 1.336285933906074
72,cbrt*2,sqrt*4:  2340 ms, 1.336285933906074

Intel Xeon E5-2637 v2 3.50GHz

このマシンのg++はversion 4.4.7と古くて、C++11のコードをコンパイルできない。 Intel C++ (icpc)の結果のみ。

$ grep Xeon /proc/cpuinfo | head -1
model name: Intel(R) Xeon(R) CPU E5-2637 v2 @ 3.50GHz
$ make clean
rm -f *.o core *.s *.ps *.ps~ pow48_g++ pow48_icpc pow48f_gfortran 00pow48.html
$ g++ --version | head -1
g++ (GCC) 4.4.7 20120313 (Red Hat 4.4.7-16)
$ make pow48_icpc
icpc -V |& head -1
Intel(R) C++ Intel(R) 64 Compiler for applications running on Intel(R) 64, Version 16.0.1.150 Build 20151021
icpc -std=c++11 -O3    pow48.cpp -o pow48_icpc
./pow48_icpc
32,pow          :  1032 ms, 1.93965617861778
32,sqrt*5       :   621 ms, 1.93965617861778
48,pow          :  1013 ms, 1.555308347057671
48,cbrt,sqrt*4  :   943 ms, 1.555308347057671
72,pow          :  1013 ms, 1.342386786199955
72,cbrt*2,sqrt*4:  1321 ms, 1.342386786199955

g++にもicpcにも-march=nativeという コンパイルオプションを指定することができる。 次のように、このマシンとコンパイラの組み合わせでのみ著しく高速化した。

$ icpc -std=c++11 -O3 -march=native pow48.cpp -o pow48_icpc
$ ./pow48_icpc
32,pow          :   631 ms, 1.93965617861778
32,sqrt*5       :   621 ms, 1.93965617861778
48,pow          :   630 ms, 1.555308347057671
48,cbrt,sqrt*4  :   821 ms, 1.555308347057671
72,pow          :   631 ms, 1.342386786199955
72,cbrt*2,sqrt*4:  1087 ms, 1.342386786199955

まとめ

GCC-6でpow(x, 1/32.)が最適化されるのを確認した。 Intel C++ (icpc)がさらに速い実行形式を作ってくれるようだ。

反省

  • より速くできるコンパイルオプションがあるかもしれない。
  • 超ひさしぶりにC++のtemplateを使ったら鼻血が出た。
  • アセンブラを読めるようにならないといけない。
  • SIMD, SSE, AVXを理解しないといけない。

参考文献

  1. Kyrill Tkachov: [PATCH][tree-ssa-math-opts] Expand pow (x, CONST) using square roots when possible https://gcc.gnu.org/ml/gcc-patches/2015-05/msg00071.html
  2. chunjp: pow(x, 1/48.0)を高速化……うーん…… https://twitter.com/chunjp/status/596940789495635968
  3. 高木、田中: 立方根計算のハードウェアアルゴリズムについて http://ci.nii.ac.jp/naid/110003226017
  4. 高木、南: 減算シフト型立方根計算回路 http://ci.nii.ac.jp/naid/110003226017
  5. 津田伸秀: std::chronoはC++11で追加された時間計測の枠組みだ http://vivi.dyndns.org/tech/cpp/timeMeasurement.html#chrono
  6. SVML (Short Vector Mathematical Library) 操作の組込み関数 http://www.xlsoft.com/jp/products/intel/compilers/manual/cpp_all_os/GUID-14A424F2-8AAE-41C0-B4A9-A494ADBD3B29.htm
# -*-Makefile-*- for pow48.cpp
##
all: pow48_g++ pow48_icpc
pow48_g++: pow48.cpp
g++ --version | head -1
g++ -std=c++11 -Ofast $< -o $@
./$@
pow48_icpc: pow48.cpp
ifeq ($(shell icpc -V 2> /dev/null; echo $$?),0)
icpc -V |& head -1
icpc -std=c++11 -O3 $< -o $@
./$@
endif
pp: pow48.cpp
LANG=C a2ps --medium=a4 --header='Takeshi Nishimatsu' --underlay=pow48.cpp\
--center-title=pow48.cpp --prologue=color --portrait --columns=1\
--borders=off -f 11.0 --pretty-print=cpp -o pow48.ps pow48.cpp
clean:
rm -f *.o core *.s *.ps *.ps~ pow48_g++ pow48_icpc pow48f_gfortran 00pow48.html
// pow48.cpp
// Copyright (C) 2015,2016 Takeshi Nishimatsu
// License: GPLv3
////
#include <cmath>
#include <iostream>
#include <iomanip>
#include <chrono>
const std::size_t N = 64*1024*1024;
template<typename _Rep, typename _Period>
void report_timing(const std::chrono::duration<_Rep, _Period>& dur,
const char *s, const double x)
{
std::cout << std::setw(16) << std::left << s << ": "
<< std::setw( 5) << std::right
<< std::chrono::duration_cast<std::chrono::milliseconds>(dur).count()
<< " ms, " << std::setprecision(16) << x << std::endl;
}
int main()
{
std::size_t i;
double* ary_in = new double[N]; // allocate them in the heap
double* ary_out = new double[N];
std::srand(123456789);
for (i=0; i<N; ++i) {
ary_in[ i] = std::rand();
ary_out[i] = 0.0; // first touch
}
auto start = std::chrono::system_clock::now();
for (i=0; i<N; ++i) { ary_out[i] = pow(ary_in[i], 1/32.); }
auto end = std::chrono::system_clock::now();
report_timing(end-start, "32,pow", ary_out[100]);
start = std::chrono::system_clock::now();
for (i=0; i<N; ++i) { ary_out[i] = sqrt(sqrt(sqrt(sqrt(sqrt(ary_in[i]))))); }
end = std::chrono::system_clock::now();
report_timing(end-start, "32,sqrt*5", ary_out[100]);
start = std::chrono::system_clock::now();
for (i=0; i<N; ++i) { ary_out[i] = pow(ary_in[i], 1/48.); }
end = std::chrono::system_clock::now();
report_timing(end-start, "48,pow", ary_out[100]);
start = std::chrono::system_clock::now();
for (i=0; i<N; ++i) { ary_out[i] = cbrt(sqrt(sqrt(sqrt(sqrt(ary_in[i]))))); }
end = std::chrono::system_clock::now();
report_timing(end-start, "48,cbrt,sqrt*4", ary_out[100]);
start = std::chrono::system_clock::now();
for (i=0; i<N; ++i) { ary_out[i] = pow(ary_in[i], 1/72.); }
end = std::chrono::system_clock::now();
report_timing(end-start, "72,pow", ary_out[100]);
start = std::chrono::system_clock::now();
for (i=0; i<N; ++i) { ary_out[i] = cbrt(cbrt(sqrt(sqrt(sqrt(ary_in[i]))))); }
end = std::chrono::system_clock::now();
report_timing(end-start, "72,cbrt*2,sqrt*4", ary_out[100]);
return 0;
}
! pow48f.f
! Copyright (C) 2015,2016 Takeshi Nishimatsu
! License: GPLv3
!!
subroutine report_timing(dur, s, x)
implicit none
integer, intent(in) :: dur
character(len=*), intent(in) :: s
real*8, intent(in) :: x
integer :: dmy, count_rate
call system_clock(dmy, count_rate)
write(6,'(a,i4,a,f20.16)') s, dur*1000/count_rate, ' ms, ', x
end subroutine report_timing
program pow48
implicit none
integer, parameter :: N=64*1024*1024
integer:: i, start, finish, count_rate
real*8 :: ary_in(N), ary_out(N)
do i=1,N
ary_in( i) = 0.2d0
ary_out(i) = 0.0
end do
call system_clock(start)
do i=1,N
ary_out(i) = ary_in(i)**(1/32.0d0)
end do
call system_clock(finish)
call report_timing(finish-start, "aaaaa", ary_out(100))
end program pow48
!local variables:
! compile-command: "gfortran -ffree-form -Ofast pow48f.f -o pow48f_gfortran && ./pow48f_gfortran"
!End:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment