測試方法
計算任務(wù):如果某數(shù)字,各位數(shù)的立方之和等于它自身,則符合條件殖演。找出 500 萬以內(nèi)所有符合條件的數(shù)锦秒。
為了測試語言本身,而非算法廉羔,上述計算不考慮剪枝判斷,用最硬的辦法做。
上述任務(wù)運行五次轴捎,測量總耗時,除以五蚕脏,作為平均耗時侦副。耗時越低越好。
測試結(jié)果
語言 | 代碼寫法 | 環(huán)境或者虛擬機 | 是否 JIT | 耗時(秒) |
---|---|---|---|---|
C++ | int | Visual Studio 2019, x64, Release | 0.038 | |
C++ | double, mod | Visual Studio 2019, x64, Release | 0.423 | |
C++ | double, without mod | Visual Studio 2019, x64, Release | 0.231 | |
C++ (AVX2) | int | Visual Studio 2019, x64, Release | 0.031 | |
C++ (AVX2) | double, without mod | Visual Studio 2019, x64, Release | 0.021 | |
C++ (emscripten, WebAssembly) | int | emscripten 2.0.29, -O3, node.js 14.17.3 | Yes | 0.129 |
C++ (emscripten, WebAssembly) | int | emscripten 2.0.29, -O3, node.js 14.17.3, --jitless | 報錯 | |
C++ (emscripten, asm.js) | int | emscripten 2.0.29, -O3, node.js 14.17.3 | Yes | 0.064 |
C++ (emscripten, asm.js) | int | emscripten 2.0.29, -O3, node.js 14.17.3, --jitless | 3.201 | |
python | int | CPython 2.7.5 | 3.923 | |
python | int | CPython 3.9.1 | 3.810 | |
python | int | pypy2.7-v7.3.5-win64 | Yes | 0.094 |
python | int | pypy3.7-v7.3.5-win64 | Yes | 0.094 |
javascript | int | node.js 14.17.3 | Yes | 0.097 |
javascript | int | node.js 14.17.3 --jitless | 1.721 | |
javascript | double, mod | node.js 14.17.3 | Yes | 0.202 |
javascript | double, mod | node.js 14.17.3 --jitless | 2.215 | |
javascript | double, without mod | node.js 14.17.3 | Yes | 0.221 |
javascript | double, without mod | node.js 14.17.3 --jitless | 2.244 | |
lua | double, mod | lua 5.4.3 | 2.477 | |
lua | double, mod | lua jit 2.0.5 | Yes | 0.115 |
lua | double, mod | lua jit 2.0.5 - without jit | 1.298 | |
lua | double, without mod | lua 5.4.3 | 2.183 | |
lua | double, without mod | lua jit 2.0.5 | Yes | 0.118 |
lua | double, without mod | lua jit 2.0.5 - without jit | 1.276 |
結(jié)論
- 開啟 JIT 的情況下驼鞭,各個腳本語言的性能都接近秦驯。最快到最慢依次是 python, js, lua
要注意 node.js 如果用了math.floor
會變慢。lua 看起來沒有這個問題挣棕。 - 關(guān)閉 JIT 的情況下译隘,各個腳本語言的性能差異大。最快到最慢依次是 lua, js, python
在本次測試中洛心,luajit 即使關(guān)閉了 JIT 仍然比 lua 快很多固耘。
但這僅僅是一個小測試,可能需要更多的測試去評估綜合性能词身。 - C++ 當然是最快的厅目,腳本難以望其項背,即便是 JIT 也有數(shù)倍的差距法严。
而且 C++ 可以采用 SIMD 可能進一步提升损敷。
但 C++ 用不好的情況下,可能不如腳本 JIT深啤。這就比較尷尬尷尬拗馒。 - 腳本是有天花板的。
如果感覺腳本慢溯街,最好的辦法是用 C++ 改寫诱桂,而不是換一種腳本語言。
換言之呈昔,選擇腳本語言時访诱,一般不應(yīng)該把性能作為主要參考。 - emscripten 把 C++ 編譯為“腳本”韩肝。技術(shù)上是行得通触菜,但是不至于真的用 C++ 來寫邏輯。
相比腳本哀峻,寫 C++ 的工作量更大涡相。
用 emscripten 編譯之后沒有明顯的性能優(yōu)勢哲泊。特別是在禁止 JIT 的平臺上。
所以是吃力不討好催蝗。
emscripten 目前的價值切威,是方便把 C++ 代碼移植到 javascript 生態(tài)”牛看起來并不適合正常開發(fā)先朦。
細節(jié)描述
是否用 JIT ,要做出區(qū)分犬缨。因為 iOS 目前基本上不能用 JIT喳魏。
不過逐漸開始放開了,特別是對于 javascript怀薛。
本計算刺彩,首要區(qū)別在于數(shù)值的類型。int 會比 double 快枝恋。
lua 和 javascript 是用 double创倔,但虛擬機可能視情況選擇用 int。
而 python 和 C++ 則同時提供了 int 和 double 供程序員選擇焚碌。
首先用 C++ 在 int 和 double 的情況下分別測試畦攘。
1. C++
一開始代碼是按 int 寫的。
在這個版本里十电,除以常數(shù) 10念搬,其實在編譯之后根本不含有除法指令,除法被改為了乘法摆出,速度有所提高。
int x = v % 10;
00007FF7B94A1090 mov eax,0CCCCCCCDh
00007FF7B94A1095 mul eax,ecx
00007FF7B94A1097 shr edx,3
00007FF7B94A109A lea eax,[rdx+rdx*4]
00007FF7B94A109D add eax,eax
00007FF7B94A109F sub ecx,eax
00007FF7B94A10A1 mov r8d,ecx
v = (v / 10);
00007FF7B94A10A4 mov ecx,edx
然后直接翻譯為 double首妖,發(fā)現(xiàn):fmod 耗時占 76%偎漫,floor 耗時占 19%。
基于此有缆,又做了一個避免求余數(shù)的版本象踊。
為了繼續(xù)提高,又做了 AVX 的版本棚壁。
可以看到在 AVX 中杯矩,double 會比 int 更快。即便 double 是四個四個處理袖外,而 int 是八個八個處理史隆。
因為 AVX 的乘除法,int 會比 double 慢曼验。特別是除法泌射,AVX 沒有整數(shù)除法指令粘姜,編譯器提供了一個函數(shù)來實現(xiàn)除法。
// 0.038 秒
__declspec(noinline)
void test_int(int N)
{
for (int i = 1; i <= N; ++i) {
int s = 0;
int v = i;
while (v > 0) {
int x = v % 10;
v = (v / 10);
s += x * x * x;
}
if (s == i) {
printf("%d\n", s);
}
}
}
// 0.423 秒
__declspec(noinline)
void test_double_with_fmod(int N)
{
double NN = N;
for (double i = 1; i <= NN; ++i) {
double s = 0.0;
double v = i;
while (v > 0) {
double x = fmod(v, 10.0);
v = floor(v / 10);
s += x * x * x;
}
if (s == i) {
printf("%d\n", i);
}
}
}
// 0.231 秒
__declspec(noinline)
void test_double_without_fmod(int N)
{
double NN = N;
for (double i = 1; i <= NN; ++i) {
double s = 0.0;
double v = i;
while (v > 0) {
double v2 = floor(v / 10);
double x = v - v2 * 10;
v = v2;
s += x * x * x;
}
if (s == i) {
printf("%d\n", i);
}
}
}
// 0.021 秒
__declspec(noinline)
void test_avx2_double(int N)
{
__declspec(align(16)) double val_arr[] = { 1.0, 2.0, 3.0, 4.0 };
__m256d val = _mm256_load_pd(val_arr);
__m256d four = _mm256_set1_pd(4.0);
__m256d ten = _mm256_set1_pd(10.0);
for (int i = 1; i <= N; i += 4, val = _mm256_add_pd(val, four)) {
__m256d s = _mm256_setzero_pd();
__m256d v = val;
for (int d = 1; d <= i + 3; d *= 10) {
__m256d v2 = _mm256_round_pd(_mm256_div_pd(v, ten), _MM_FROUND_FLOOR);
__m256d x = _mm256_sub_pd(v, _mm256_mul_pd(v2, ten));
v = v2;
s = _mm256_add_pd(_mm256_mul_pd(_mm256_mul_pd(x, x), x), s);
}
int eq = _mm256_movemask_pd(_mm256_cmp_pd(val, s, _CMP_EQ_OQ));
if (eq != 0) {
for (int v = 0; v < 4; ++v) {
if ((eq & (1 << v)) != 0) {
printf("%d\n", i + v);
}
}
}
}
}
// 0.031 秒
__declspec(noinline)
void test_avx2_int(int N)
{
int val_arr[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
__m256i val = _mm256_loadu_si256(reinterpret_cast<__m256i*>(val_arr));
__m256i eight = _mm256_set1_epi32(8);
__m256i ten = _mm256_set1_epi32(10);
for (int i = 1; i <= N; i += 8, val = _mm256_add_epi32(val, eight)) {
__m256i s = _mm256_setzero_si256();
__m256i v = val;
for (int d = 1; d <= i + 7; d *= 10) {
__m256i v2 = _mm256_div_epi32(v, ten);
__m256i x = _mm256_sub_epi32(v, _mm256_mullo_epi32(v2, ten));
v = v2;
s = _mm256_add_epi32(_mm256_mullo_epi32(_mm256_mullo_epi32(x, x), x), s);
}
int eq = _mm256_movemask_epi8(_mm256_cmpeq_epi32(val, s));
if (eq != 0) {
for (int v = 0; v < 8; ++v) {
if ((eq & (1 << (v*4))) != 0) {
printf("%d\n", i + v);
}
}
}
}
}
2. C++ and emscripten
這種奇怪的組合允許動態(tài)加載 C++ 模塊熔酷。
意味著 C++ 代碼可以做成 patch 外放孤紧,而且如果用 asm.js 的話還不用擔心權(quán)限問題,就像腳本一樣拒秘。
實測發(fā)現(xiàn) asm.js 竟然是 JIT 里最快的号显。但是關(guān)掉 jit 之后就很慢,甚至遠不如直接寫 js躺酒。
WebAssembly 速度不如 asm.js押蚤,是我沒想到的。
原因不太清楚阴颖。但事實如此活喊。
總之這些測試結(jié)果比較違反直覺,可能是因為我不熟悉吧量愧。
另外:WebAssembly 在關(guān)閉 JIT 的情況下是無法使用的钾菊,需要注意。
// compile to WebAssembly: emcc main.cpp -O3
// compile to asm.js: emcc main.cpp -O3 -s WASM=0
#include <chrono>
#include <cstdio>
void test_calculate_int(int N)
{
for (int i = 1; i <= N; ++i) {
int s = 0;
int v = i;
while (v > 0) {
int x = v % 10;
v = (v / 10);
s += x * x * x;
}
if (s == i) {
printf("%d\n", s);
}
}
}
int main()
{
std::chrono::high_resolution_clock::time_point t1, t2;
t1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 5; ++i)
{
test_calculate_int(5000000);
}
t2 = std::chrono::high_resolution_clock::now();
printf("execution cost: %.3f seconds\n", double(std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()) / 5.0 / 1000.0);
return 0;
}
3. python
python 本身有 int偎肃,不必使用 double 類型了煞烫。只測試了一種寫法。
import timeit
import platform
version = platform.python_version_tuple()[0]
if version == '2':
my_range = xrange
else:
my_range = range
def test(N):
for i in my_range(1, N + 1):
s = 0
v = i
while v > 0:
x = v % 10
v //= 10
s += x * x * x
if s == i:
print(s)
cost = timeit.timeit(lambda: test(5000000), number=5)
print(cost / 5)
4. javascript
javascript 雖然標準規(guī)定了數(shù)值計算用 double累颂。
但 node.js 虛擬機在一些情況下會幫你選擇用 int滞详。
例如:某變量只作為 for 循環(huán),每次 ++i紊馏。
又例如:某變量每次賦值的時候都是用 |0 這樣的操作來取整料饥。
本例中,把 Math.floor(x)
改為 x | 0
就能觀察到性能的成倍提升朱监。
注意這里的 test_double 比 C++ 寫的 double 版本更快岸啡。
原因是 C++ 在 for 循環(huán)的變量 i 也用了 double 類型,而這里的 i 被 nodejs 做成了 int 類型赫编。
另外:如果避免求余巡蘸,并不會像 C++ 避免求余那樣導致成倍的性能差異。
可能是因為虛擬機看到 v = i
然后 x = v % 10
而 i 是整數(shù)擂送,所以這里用了整數(shù)求余了悦荒。
而 Math.floor 由于是內(nèi)置函數(shù),不會再被 JIT 優(yōu)化嘹吨,仍然老老實實的調(diào)用了 C 函數(shù)搬味。
另外:把 Math.floor 緩存為局部變量,并沒有觀測到性能提升。這跟 lua 不太一樣身腻。
function test_int(N) {
for (let i = 1; i <= N; ++i) {
let s = 0;
let v = i | 0;
while (v > 0) {
let x = v % 10;
v = (v / 10) | 0;
s += x * x * x;
}
if (s == i) {
console.log(s);
}
}
}
function test_double(N) {
for (let i = 1; i <= N; ++i) {
let s = 0;
let v = i;
while (v > 0) {
let x = v % 10;
v = Math.floor(v / 10);
s += x * x * x;
}
if (s == i) {
console.log(s);
}
}
}
function test_double_without_mod(N) {
for (let i = 1; i <= N; ++i) {
let s = 0;
let v = i;
while (v > 0) {
let v2 = Math.floor(v / 10);
let x = v - v2 * 10;
s += x * x * x;
v = v2;
}
if (s == i) {
console.log(s);
}
}
}
let t1, t2;
t1 = Date.now();
for (let i = 0; i < 5; ++i) {
test_int(5000000);
}
t2 = Date.now();
console.log(`cost time: ${(t2 -t1) / 5 / 1000}`);
5. lua
luajit 跟 node.js 類似产还,也會嘗試使用 int 類型代替 double 類型。
但是官方的 lua 則不會嘀趟。
因此脐区,在使用和不使用求余的情況下,官方的 lua 會看出性能差異她按,而 luajit 基本沒有差異牛隅。
另外:luajit 即便關(guān)掉了 JIT,仍然會比官方 lua 快得多酌泰。這可能因為它嘗試了 int媒佣。
function test_mod(N)
local floor = math.floor
for i = 1, N do
local v = i
local s = 0
while v > 0 do
x = v % 10
s = s + x * x * x
v = floor(v / 10)
end
if s == i then
print(s)
end
end
end
function test_without_mod(N)
local floor = math.floor
for i = 1, N do
local v = i
local s = 0
while v > 0 do
local v2 = floor(v / 10)
x = v - v2 * 10
s = s + x * x * x
v = v2
end
if s == i then
print(s)
end
end
end
for i = 1, 5 do
test_mod(5000000)
end