编译器别名假设、原始ptr、唯一ptr和__restrict-奇怪的结果



查看生成的asm,使用vector<int>添加第三个函数,并在ptr值相同或不同时对其进行计时后,在不使用__restrict的情况下,所有3个函数都能以最佳方式工作。请参阅我添加的答案,其中包括unique_ptr和vector版本生成了相同的代码。

问题是否有某种方法可以使用__restrict或其他技术来消除执行速度慢的问题,并允许正常使用多个unique_ptrs,而不必使用get()方法来发送原始指针。编译器难道不应该假设unique_ptrs没有别名吗?因为你不可能有部分重叠,而完全重叠是显而易见的?这与其他编译器不同吗?

我正在探索在某些情况下,如果函数被传递了原始指针,unique_ptr是否会得到更好的优化。最大优化时的MSVC编译器仍然假设用两个1unique_ptrs1调用相同类型数组的函数可能会别名。但我认为两个唯一的ptr会提供更好的优化,因为两个地址不相同的唯一ptr不可能有重叠的数组。因此,独特的ptr不仅会像原始ptr一样快,而且可能更快。

测试函数采用2个"指针"和一个指向的数组长度。这些函数被强制实例化并通过函数指针调用,因为编译器确实识别出在行中出现混叠并进行优化。

这是两个功能:

#define TYPEMOD// __restrict // Uncomment to add __restrict
void f1(int * TYPEMOD p1, int * TYPEMOD p2, int count)
{
for (int i = 1; i < count - 1; i++)
p2[i] = p1[i-1] + p1[i+1];
}
void f2(std::unique_ptr<int[]>& TYPEMOD p1, std::unique_ptr<int[]>& TYPEMOD p2, int count)
{
for (int i = 1; i < count - 1; i++)
p2[i] = p1[i-1] + p1[i+1];
}

作为参考,当数据为0246时,编译器假设这两个ptr没有别名(重叠的数组)。当数据为0259时,编译器假设存在混叠,因此如果担心可能发生了变化,则会重新读取以前的元素。

结果如下:

Raw pointer data 0259  time 0.190027
Unique_ptr  data 0259  time 0.198208

这两个函数都假设使用此编译器进行混叠,因此没有进行优化,并且唯一的ptr函数速度稍慢。

然后我看了MSVC的__restrictC++扩展。认为那样会有所帮助。应用于原始ptr和唯一ptr的结果如下:

Raw pointer data 0246  time 0.0594369
Unique_ptr  data 0259  time 0.192284

好的,unique_ptr在所有条件下都较慢,尽管在不使用__restrict的情况下非常接近原始指针。当使用__restrict修饰符时,原始指针函数版本开始生效。unique_ptr函数忽略__restrict。如果指针别名,齿轮可能会磨损,但我的生产代码很少(或根本没有)做到这一点。

结论:看来我要复习代码中的一些关键部分,以了解具有多指针、原始和唯一的函数。这种差异太大了,不容忽视。看起来在被调用的函数中使用唯一的ptrs get()方法和__restrict原始指针是非常有效的。

VC++15.9.2版,编译器选项:/permission-/GS/W3/Gy/Zc:wchar_t/Zi/Gm-/O2/sdl/Fd"x64\Release \vc141.pdb"/Zc:inline/fp:precise/D"NDEBUG"/D"_CONSOLE"/D"_UNICODE"/D"UNICODE"/errorReport:提示/WX-/Zc:forScope/Gd/Oi/MD/std:c++17/FC/Fa"x64\ Release \"/EHsc/nologo/Fo"x64\Irelease\"/诊断:经典

// Full Code
#include <iostream>
#include <memory>
#include <chrono>
class Timer {
using clock = std::chrono::system_clock;
double cumulative_time{};
double interval_time{};
clock::time_point snapshot_time{ clock::now() }, tmp;
public:
void start() { snapshot_time = clock::now(); }
void reset() { cumulative_time = 0; start(); }
double get_interval_time() {
cumulative_time += (interval_time = std::chrono::duration<double>((tmp = clock::now()) - snapshot_time).count());
snapshot_time = tmp;
return interval_time;
}
double get_cumulative_time() {
cumulative_time += std::chrono::duration<double>((tmp = clock::now()) - snapshot_time).count();
snapshot_time = tmp;
return cumulative_time;
}
};
template<typename T>
void fill(T &v, int len) {
int i = 0;
for (int i = 0; i < len; i++)
v[i] = i;
}

using namespace std;
#define TYPEMOD  //__restrict // Uncomment to add __restrict
void f1(int * TYPEMOD p1, int * TYPEMOD p2, int count)
{
for (int i = 1; i < count - 1; i++)
p2[i] = p1[i - 1] + p1[i + 1];
}
void f2(std::unique_ptr<int[]>& TYPEMOD p1, std::unique_ptr<int[]>& TYPEMOD p2, int count)
{
for (int i = 1; i < count - 1; i++)
p2[i] = p1[i - 1] + p1[i + 1];
}

auto xf1 = f1;  // avoid inlining
auto xf2 = f2;

int main() {
const int N = 100'000'000;
auto pa = new int[N]; fill(pa, N);
auto ptra = std::make_unique<int[]>(N); fill(ptra, N);
Timer timer;
xf1(pa, pa, N);
auto snap1 = timer.get_interval_time();
xf2(ptra, ptra, N);
auto snap2 = timer.get_interval_time();
std::cout << "Raw pointer data " << pa[0] << pa[1] << pa[2] << pa[3] << "  time " << snap1 << "n";
std::cout << "Unique_ptr  data " << ptra[0] << ptra[1] << ptra[2] << ptra[3] << "  time " << snap2 << "n";
std::cout << "n";
}

我深入研究了MSVC如何在有别名和无别名的情况下进行优化,并包含了名为f3()的测试函数的vector<int>版本。现在设置为:

void f1(int *p1, int *p2, int count)
{
for (int i = 1; i < count - 1; i++)
p2[i] = p1[i - 1] + p1[i + 1];
}
void f2(std::unique_ptr<int[]>& p1, std::unique_ptr<int[]>& p2, int count)
{
for (int i = 1; i < count - 1; i++)
p2[i] = p1[i - 1] + p1[i + 1];
}
void f3(vector<int>& p1, vector<int>& p2, int count)
{
for (int i = 1; i < count - 1; i++)
p2[i] = p1[i - 1] + p1[i + 1];
}
auto xf1 = f1;  // avoid inlining
auto xf2 = f2;
auto xf3 = f3;

和以前一样,xf1、xf2和xf3强制实例化函数,并在编译器希望内联它们时提供调用它们的指针。

事实证明,unique_ptr和矢量版本(f2()和f3())生成的代码可以测试p1和p2是否指向同一内存,如果是,则假设混叠生成缓慢但正确的代码。

有趣的是unique_ptr<int>[]vector<int>产生相同的代码。当链接器优化器中启用COMDAT代码折叠时,重复项被删除,函数指针xf3被设置为与xf2相同的地址,这可以在调试中看到。所以当f3被调用时,它实际上是被执行的f2代码。

当执行这些功能时,它们首先测试p1和p2是否相同。如果是这样的话,他们会假设混叠并生成正确的代码,否则他们会假设没有混叠并产生更快的代码。如果在f1()中使用__restrict,则代码不会首先测试p1和p2是否相同,而是直接进入假定没有混叠的代码。

总之,除了p1和p2指向同一地址的情况外,__restrict并没有真正加快原始ptr函数的速度。当p1和p2不同时,其速度与unique_ptr和向量版本一样快。

即使在调用原始指针函数时,编译器也会生成快速代码,这是以指针等价性的初始测试为代价的。当指针相等时,编译器会假设出现混叠,从而降低测试速度。

有没有办法使用__restrict或其他技术来消除执行缓慢的

是。只需将指针强制转换为restrict,从而向编译器提供它们彼此受限的信息。

#include <memory>
#include <vector>
#if defined(__cplusplus)
#if defined(_MSC_VER)
#define restrict __restrict
#elif defined(__GNUC__)
#define restrict __restrict__
#endif
#endif
void f1(int * restrict p1, int * restrict p2, int count)
{
for (int i = 1; i < count - 1; i++)
p2[i] = p1[i - 1] + p1[i + 1];
}
void f2(std::unique_ptr<int[]>& pp1, std::unique_ptr<int[]>& pp2, int count)
{
int * const restrict p1 = pp1.get();
int * const restrict p2 = pp2.get();
for (int i = 1; i < count - 1; i++)
p2[i] = p1[i - 1] + p1[i + 1];
}
void f3(std::vector<int>& pp1, std::vector<int>& pp2, int count)
{
int * const restrict p1 = &pp1[0];
int * const restrict p2 = &pp2[0];
for (int i = 1; i < count - 1; i++)
p2[i] = p1[i - 1] + p1[i + 1];
}

但代码复制是最糟糕的:

void f1(int * restrict p1, int * restrict p2, int count)
{
for (int i = 1; i < count - 1; i++)
p2[i] = p1[i - 1] + p1[i + 1];
}
void f2(std::unique_ptr<int[]>& pp1, std::unique_ptr<int[]>& pp2, int count)
{
f1(pp1.get(), pp2.get(), count);
}
void f3(std::vector<int>& pp1, std::vector<int>& pp2, int count)
{
f1(&pp1[0], &pp2[0], count);
}

编译器难道不应该假设unique_ptrs没有别名吗?因为你不能有部分重叠,而完全重叠是显而易见的?

否。如图所示,我们可以使用std::unique_ptr::get()函数来获取指针。这么做:

std::unique_ptr p1;int*a=p1.get();int*b=p1.get();f1(a,b,5);

将创建指向同一内存的三个指针。

这与其他编译器不同吗?

当然可以。C++中不支持restrict。这只是编译器的一个提示,编译器可能会忽略它

这种差异太大了,无法忽略

唯一的比较方法是比较生成的程序集代码。我没有视觉工作室,所以我做不到。

class Timer { using clock = std::chrono::system_clock;

system_clock是一款全系统实时挂钟。挂钟用于在桌面(墙上)上显示用户好看的时间。这就是为什么它被称为"挂钟",它应该被用来展示在墙上。使用单调时钟,如high_resolution_clock,用于测量间隔。或者最好不要比较执行速度,这取决于环境。比较指令计数,例如调试器测量的指令数与样本数据或最佳指令数,计算编译器为特定体系结构和编译器选项生成的汇编指令数。像godbolt这样的网站经常派上用场。

restrict限定符是程序员要注意的。您必须确信,您没有将指针传递到重叠的区域。

您可以在gcc-O2下编译代码,无论是否限制为相同的汇编指令,请参阅此处。如果您希望加快执行速度,请开始使用特定于平台的指令。

相关内容

  • 没有找到相关文章

最新更新