为什么在这种情况下,我的Java代码比我的C++代码运行得更快?
我写了一个小基准,其中程序创建了 的 10 个8二维std::vector
结构{float, float}
,然后将它们的长度平方相加。
这是C++代码:
#include <iostream>
#include <chrono>
#include <vector>
#include <array>
#include <cmath>
using namespace std;
using namespace std::chrono;
const int COUNT = pow(10, 8);
class Vec {
public:
float x, y;
Vec() {}
Vec(float x, float y) : x(x), y(y) {}
float len() {
return x * x + y * y;
}
};
int main() {
vector <Vec> vecs;
for(int i = 0; i < COUNT; ++i) {
vecs.emplace_back(i / 3, i / 5);
}
auto start = high_resolution_clock::now();
// This loop is timed
float sum = 0;
for(int i = 0; i < COUNT; ++i) {
sum += vecs[i].len();
}
auto stop = high_resolution_clock::now();
cout << "finished in " << duration_cast <milliseconds> (stop - start).count()
<< " milliseconds" << endl;
cout << "result: " << sum << endl;
return 0;
}
为此,我使用了这个 makefile(g++ 版本 7.5.0):
build:
g++ -std=c++17 -O3 main.cpp -o program #-ffast-math
run: build
clear
./program
这是我的Java代码:
public class MainClass {
static final int COUNT = (int) Math.pow(10, 8);
static class Vec {
float x, y;
Vec(float x, float y) {
this.x = x;
this.y = y;
}
float len() {
return x * x + y * y;
}
}
public static void main(String[] args) throws InterruptedException {
Vec[] vecs = new Vec[COUNT];
for (int i = 0; i < COUNT; ++i) {
vecs[i] = new Vec(i / 3, i / 5);
}
long start = System.nanoTime();
// This loop is timed
float sum = 0;
for (int i = 0; i < COUNT; ++i) {
sum += vecs[i].len();
}
long duration = System.nanoTime() - start;
System.out.println("finished in " + duration / 1000000 + " milliseconds");
System.out.println("result: " + sum);
}
}
使用 Java 11.0.4 编译和运行
以下是结果(在 ubuntu 18.04 16 位上运行几次的平均值):
c++: 262 ms
java: 230 ms
为了使 C++ 代码更快,我尝试了以下几点:
- 使用
std::array
代替std::vector
- 使用普通数组而不是
std::vector
- 在
for
循环中使用迭代器
然而,以上都没有导致任何改善。
我注意到一些有趣的事情:
- 当我对整个
main()
函数(分配 + 计算)计时时,C++ 要好得多。但是,这可能是由于 JVM 的预热时间。 - 对于较少数量的对象,例如 10 7,C++ 稍微快一些(几毫秒)。
- 开启
-ffast-math
使 C++ 程序比 Java 快几倍,但计算结果略有不同。此外,我在一些帖子中读到使用这个标志是不安全的。
在这种情况下,我能否以某种方式修改我的 C++ 代码并使其与 Java 一样快或更快?
回答
尝试这个:
float sum = std::transform_reduce(
std::execution::par_unseq,
begin(vecs), end(vecs),
0.f,
std::plus<>{},
[](auto&& x){
return x.len();
}
);
这明确地告诉 C++ 编译器你在做什么,你可以使用额外的线程,每个循环迭代不依赖于其他的,并且你想在float
s 中完成工作。
这确实意味着与您要求的相比,添加可能会发生顺序错误,因此输出值可能不完全相同。
一边是原始循环的现场示例,另一边是无序添加的权限。
进一步的调查:
所以我旋转了一个godbolt。
在其中,我比较了使用和不使用强制矢量化和-ffast-math
. 强制矢量化并-ffast-math
导致相同的汇编代码。
问题是蓄能器。一次将一个东西添加到总和中并进行所有 IEEE 舍入会给你一个不同的值,而不是在更高精度的浮点值中一次累加 N 个,然后将结果批量存储回浮点数。
如果你这样做,-ffast-math
你将获得 2 倍的速度和不同的积累。如果更换float sum
用double sum
,你会得到相同的答案是--ffast-math
和vectorizaton。
基本上,clang 向量化器没有看到一种简单的方法来向量化和的累加而不破坏精确的浮点精度浮点要求。