我可以通过CPU分析器看到compute_variances()
是我项目的瓶颈。
% cumulative self self total
time seconds seconds calls ms/call ms/call name
75.63 5.43 5.43 40 135.75 135.75 compute_variances(unsigned int, std::vector<Point, std::allocator<Point> > const&, float*, float*, unsigned int*)
19.08 6.80 1.37 readDivisionSpace(Division_Euclidean_space&, char*)
...
这是函数的主体:
void compute_variances(size_t t, const std::vector<Point>& points, float* avg,
float* var, size_t* split_dims) {
for (size_t d = 0; d < points[0].dim(); d++) {
avg[d] = 0.0;
var[d] = 0.0;
}
float delta, n;
for (size_t i = 0; i < points.size(); ++i) {
n = 1.0 + i;
for (size_t d = 0; d < points[0].dim(); ++d) {
delta = (points[i][d]) - avg[d];
avg[d] += delta / n;
var[d] += delta * ((points[i][d]) - avg[d]);
}
}
/* Find t dimensions with largest scaled variance. */
kthLargest(var, points[0].dim(), t, split_dims);
}
其中kthLargest()
似乎不是问题,因为我看到了:
0.00 7.18 0.00 40 0.00 0.00 kthLargest(float*, int, int, unsigned int*)
compute_variances()
采用浮点数向量的向量(即Points
的向量,其中Points
是我已实现的类)并计算每个维度中它们的方差(关于Knuth的算法)。
以下是我如何调用该函数:
float avg[(*points)[0].dim()];
float var[(*points)[0].dim()];
size_t split_dims[t];
compute_variances(t, *points, avg, var, split_dims);
问题是,我能做得更好吗?我真的很乐意在速度和差异的近似计算之间进行权衡。或者也许我可以使代码更加缓存友好或什么?
我这样编译:
g++ main_noTime.cpp -std=c++0x -p -pg -O3 -o eg
请注意,在编辑之前,我使用了-o3
,而没有使用大写&#39; o&#39;。感谢 ypnos ,我现在使用优化标记-O3
编译。我确信它们之间存在差异,因为我在伪网站中使用these methods之一进行了时间测量。
请注意,现在compute_variances
占据了整个项目的时间!
[编辑]
copute_variances()
被召唤40次。
每10次通话,以下情况属实:
points.size() = 1000 and points[0].dim = 10000
points.size() = 10000 and points[0].dim = 100
points.size() = 10000 and points[0].dim = 10000
points.size() = 100000 and points[0].dim = 100
每个调用都处理不同的数据。
问:访问points[i][d]
的速度有多快?
答:point[i]
只是std :: vector的第i个元素,其中第二个[]
在Point
类中实现。
const FT& operator [](const int i) const {
if (i < (int) coords.size() && i >= 0)
return coords.at(i);
else {
std::cout << "Error at Point::[]" << std::endl;
exit(1);
}
return coords[0]; // Clear -Wall warning
}
其中coords
是std::vector
float
个值。这似乎有点沉重,但编译器是否应该足够聪明以正确预测分支始终为真? (我的意思是冷启动后)。此外,std::vector.at()
应该是恒定时间(如ref中所述)。我将此更改为在函数体中只有.at()
,并且时间测量保持不变,几乎相同。
compute_variances()
中的分区肯定是重的!然而,Knuth的算法是一个数值稳定的算法,我无法找到另一种算法,即数值稳定且没有除法。
请注意,我现在并行 。
[EDIT.2]
Point
班的最小例子(我想我没有忘记展示一些东西):
class Point {
public:
typedef float FT;
...
/**
* Get dimension of point.
*/
size_t dim() const {
return coords.size();
}
/**
* Operator that returns the coordinate at the given index.
* @param i - index of the coordinate
* @return the coordinate at index i
*/
FT& operator [](const int i) {
return coords.at(i);
//it's the same if I have the commented code below
/*if (i < (int) coords.size() && i >= 0)
return coords.at(i);
else {
std::cout << "Error at Point::[]" << std::endl;
exit(1);
}
return coords[0]; // Clear -Wall warning*/
}
/**
* Operator that returns the coordinate at the given index. (constant)
* @param i - index of the coordinate
* @return the coordinate at index i
*/
const FT& operator [](const int i) const {
return coords.at(i);
/*if (i < (int) coords.size() && i >= 0)
return coords.at(i);
else {
std::cout << "Error at Point::[]" << std::endl;
exit(1);
}
return coords[0]; // Clear -Wall warning*/
}
private:
std::vector<FT> coords;
};
答案 0 :(得分:3)
<强> 1。 SIMD 强>
一个简单的加速就是使用向量指令(SIMD)进行计算。在x86上,这意味着SSE,AVX指令。根据您的字长和处理器,您可以获得大约x4甚至更多的加速。这段代码在这里:
for (size_t d = 0; d < points[0].dim(); ++d) {
delta = (points[i][d]) - avg[d];
avg[d] += delta / n;
var[d] += delta * ((points[i][d]) - avg[d]);
}
通过使用SSE一次计算四个元素,可以加速。由于您的代码实际上只在每个循环迭代中处理一个元素,因此没有瓶颈。如果你下降到16位短而不是32位浮点数(那么近似值),你可以在一条指令中输入8个元素。使用AVX它会更多,但你需要一个最新的处理器。
它不是解决您的性能问题的 解决方案,而只是其中一个也可以与其他人结合使用。
<强> 2。微parallelizm 强>
当你拥有那么多循环时,第二个简单的加速就是使用并行处理。我通常使用英特尔TBB,其他人可能会建议使用OpenMP。为此,您可能需要更改循环顺序。因此,在外循环中并行化d,而不是在i。
你可以结合使用这两种技术,如果你做得对,在带有HT的四核中你可以获得25-30的加速度,而不会有任何精度损失。
第3。编译器优化
首先,也许这只是SO上的一个错字,但它必须是-O3
,而不是-o3
!
总的来说,如果在实际使用它们的范围内声明变量delta,n,编译器可能更容易优化代码。您还应该尝试-funroll-loops
编译器选项以及-march
。后者的选项取决于你的CPU,但现在通常-march core2
很好(也适用于最近的AMD),并且包括SSE优化(但我不相信编译器只是为你的循环做这件事)。 / p>
答案 1 :(得分:3)
您的数据结构存在的一个主要问题是它本质上是vector<vector<float> >
。这是一个指向float
数组的指针数组的指针,附有一些铃声和口哨声。特别是,访问Point
中的连续vector
s对应于访问连续的存储器位置。我敢打赌,当您分析此代码时,您会看到大量的缓存未命中。
在用其他任何东西徘徊之前解决这个问题。
低阶关注点包括内循环中的浮点除法(在外循环中计算1/n
)和作为内循环的大型加载存储链。例如,您可以使用SIMD计算数组切片的均值和方差,并将它们组合在一起。
每次访问一次的边界检查可能也没有帮助。摆脱它,或者至少将它从内环中提升出来;不要假设编译器知道如何自行修复它。
答案 2 :(得分:1)
这就是我要做的事情,按照估计的重要性顺序:
Point::operator[]
按值返回浮点,而不是按引用返回。coords[i]
代替coords.at(i)
,因为已经断言它在界限范围内。 at
成员检查边界。你只需要检查一次。Point::operator[]
中的自制错误指示/检查。这就是断言的意义所在。它们在发布模式下名义上是无操作的 - 我怀疑你需要在发布代码中检查它。avg
和var
。事实上,如果prefetch按照相反的迭代顺序工作,它可能会删除avg
和var
上的所有缓存未命中。std::fill
和std::copy
可以利用类型对齐,并且有可能比C库memset
和memcpy
更快。 Point::operator[]
将有可能在发布版本中加入内联,并且可以减少到两个机器指令(有效地址计算和浮点加载)。那正是你想要的。当然,它必须在头文件中定义,否则只有在启用链接时代码生成(a.k.a. LTO)时才会执行内联。
请注意,Point::operator[]
的正文仅相当于单行
调试版本中的return coords.at(i)
。在发布版本中,整个正文等同于return coords[i]
,不 return coords.at(i)
。
FT Point::operator[](int i) const {
assert(i >= 0 && i < (int)coords.size());
return coords[i];
}
const FT * Point::constData() const {
return &coords[0];
}
void compute_variances(size_t t, const std::vector<Point>& points, float* avg,
float* var, size_t* split_dims)
{
assert(points.size() > 0);
const int D = points[0].dim();
// i = 0, i_n = 1
assert(D > 0);
#if __cplusplus >= 201103L
std::copy_n(points[0].constData(), D, avg);
#else
std::copy(points[0].constData(), points[0].constData() + D, avg);
#endif
// i = 1, i_n = 0.5
if (points.size() >= 2) {
assert(points[1].dim() == D);
for (int d = D - 1; d >= 0; --d) {
float const delta = points[1][d] - avg[d];
avg[d] += delta * 0.5f;
var[d] = delta * (points[1][d] - avg[d]);
}
} else {
std::fill_n(var, D, 0.0f);
}
// i = 2, ...
for (size_t i = 2; i < points.size(); ) {
{
const float i_n = 1.0f / (1.0f + i);
assert(points[i].dim() == D);
for (int d = 0; d < D; ++d) {
float const delta = points[i][d] - avg[d];
avg[d] += delta * i_n;
var[d] += delta * (points[i][d] - avg[d]);
}
}
++ i;
if (i >= points.size()) break;
{
const float i_n = 1.0f / (1.0f + i);
assert(points[i].dim() == D);
for (int d = D - 1; d >= 0; --d) {
float const delta = points[i][d] - avg[d];
avg[d] += delta * i_n;
var[d] += delta * (points[i][d] - avg[d]);
}
}
++ i;
}
/* Find t dimensions with largest scaled variance. */
kthLargest(var, D, t, split_dims);
}
答案 3 :(得分:0)
for (size_t d = 0; d < points[0].dim(); d++) {
avg[d] = 0.0;
var[d] = 0.0;
}
只需使用memset即可优化此代码。在32位中的0.0的IEEE754表示是0x00000000。如果尺寸很大,那就值得了。 类似的东西:
memset((void*)avg, 0, points[0].dim() * sizeof(float));
在你的代码中,你有很多对点[0] .dim()的调用。最好在函数开头调用一次并存储在变量中。可能,编译器已经这样做了(因为你使用的是-O3)。
除了其他操作(加法,减法)之外,除法运算(从时钟周期POV开始)要贵得多。
avg[d] += delta / n;
尝试减少分割数量是有意义的:使用部分非累积平均值计算,这将导致N个元素的 Dim 除法运算(而不是 N x暗淡); N&lt; points.size()
使用Cuda或OpenCL可以实现巨大的加速,因为avg和var的计算可以针对每个维度同时进行(考虑使用GPU)。
答案 4 :(得分:0)
另一项优化是缓存优化,包括数据缓存和指令缓存。
High level optimization techniques
Data Cache optimizations
数据缓存优化示例&amp;展开
for (size_t d = 0; d < points[0].dim(); d += 4)
{
// Perform loading all at once.
register const float p1 = points[i][d + 0];
register const float p2 = points[i][d + 1];
register const float p3 = points[i][d + 2];
register const float p4 = points[i][d + 3];
register const float delta1 = p1 - avg[d+0];
register const float delta2 = p2 - avg[d+1];
register const float delta3 = p3 - avg[d+2];
register const float delta4 = p4 - avg[d+3];
// Perform calculations
avg[d + 0] += delta1 / n;
var[d + 0] += delta1 * ((p1) - avg[d + 0]);
avg[d + 1] += delta2 / n;
var[d + 1] += delta2 * ((p2) - avg[d + 1]);
avg[d + 2] += delta3 / n;
var[d + 2] += delta3 * ((p3) - avg[d + 2]);
avg[d + 3] += delta4 / n;
var[d + 3] += delta4 * ((p4) - avg[d + 3]);
}
这与传统的循环展开不同之处在于,矩阵的加载是作为循环顶部的一组执行的。
编辑1:
微妙的数据优化是将avg
和var
放入结构中。这将确保两个数组在内存中彼此相邻,无填充。处理器中的数据提取机制,例如彼此非常接近的基准。数据缓存未命中的可能性更小,并且更有可能将所有数据加载到缓存中。
答案 5 :(得分:0)
您可以使用固定点数学而不是浮点数学作为优化。
通过固定点优化
处理器喜欢操纵整数(有符号或无符号)。由于零件的提取,执行数学运算,然后重新组装零件,浮点可能需要额外的计算能力。一种缓解方法是使用 Fixed Point 数学。
简单示例:米
给定米的单位,可以通过使用浮点(例如3.14159米)来表示小于一米的长度。然而,相同的长度可以以更精细的细节为单位表示,例如毫米,例如, 3141.59毫米。为了获得更精细的分辨率,选择较小的单位并将该值乘以,例如, 3,141,590微米(微米)。关键是选择足够小的单位来将浮点精度表示为整数。
浮点值在输入时转换为固定点。所有数据处理都在固定点进行。固定点值在输出之前转换为浮点。
2个固定点基地的力量
与从浮点计转换为固定点毫米一样,使用1000,可以使用2而不是1000的幂。选择2的幂允许处理器使用位移而不是乘法或除法。以2的幂移位通常比乘法或除法更快。
保持毫米的主题和准确度,我们可以使用1024作为基础而不是1000.同样,为了更高的准确度,使用65536或131072.
<强>摘要强>
将设计或实现更改为使用的定点数学运算,允许处理器使用比浮点更多的整数数据处理指令。除了专用处理器之外,浮点运算比集成运算消耗更多的处理能力。使用2的幂作为基(或分母)允许代码使用位移而不是乘法或除法。除法和乘法比移位需要更多的操作,因此移位更快。因此,不是优化执行代码(例如循环展开),而是可以尝试使用固定点表示法而不是浮点数。
答案 6 :(得分:0)
第1点。 您可以同时计算平均值和方差。 是对的吗? 你不必首先计算平均值,然后一旦你知道它,计算平均值的平方和的平方和? 除了正确之外,它更有可能帮助表现而不是伤害它。 尝试在一个循环中做两件事并不一定比两个连续的简单循环快。
第2点。 您是否意识到有一种方法可以同时计算平均值和方差,如下所示:
double sumsq = 0, sum = 0;
for (i = 0; i < n; i++){
double xi = x[i];
sum += xi;
sumsq += xi * xi;
}
double avg = sum / n;
double avgsq = sumsq / n
double variance = avgsq - avg*avg;
第3点。 内循环正在进行重复索引。 编译器可能能够将其优化为最小化,但我不会打赌它。
第4点。 您正在使用 gprof 或类似的东西。 唯一合理可靠的数字是按功能自行计时。 它不会告诉你如何在函数内部花费时间。 我和其他许多人都依赖this method,这会让你直接找到需要时间的核心。