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

时间:2018-11-25 06:32:31

标签: c++

查看生成的汇编后,使用vector<int>添加第三个函数,并在ptrs值相同或不同时对它们进行计时,所有这三个函数在不使用__restrict的情况下几乎可以最佳地工作。请参阅我添加的答案,其中包括unique_ptr和矢量版本产生的代码相同的事实。

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

我一直在探索在某些情况下将函数传递给原始指针的unique_ptrs是否可以得到更好的优化。在最大优化状态下,MSVC编译器仍假定对相同类型的数组使用两个1unique_ptrs1调用的函数可能会别名。但是我认为,两个唯一的ptrs可以提供更好的优化,因为没有相同地址的两个唯一的ptrs不可能有重叠的数组。因此,唯一的ptrs不仅会和原始ptrs一样快,而且可能会更快。

测试函数需要2个“指针”和要指向的数组长度。这些函数是通过函数指针强制实例化和调用的,因为编译器在内联和优化时确实会识别出别名。

这是两个功能:

unique_ptr

作为参考,当数据为0246时,编译器假定两个ptr不混叠(重叠数组)。当数据为0259时,编译器将假定为别名,因此如果担心可能已更改,它将重新读取以前的元素。

结果如下:

#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];
}

这两个函数都假定使用此编译器作为别名,因此未进行优化,因此独特的ptr函数会稍微慢一些。

因此,然后我看了MSVC的Raw pointer data 0259 time 0.190027 Unique_ptr data 0259 time 0.198208 C ++扩展。认为会有所帮助。结果适用于原始ptrs和唯一ptrs:

__restrict

好的,Raw pointer data 0246 time 0.0594369 Unique_ptr data 0259 time 0.192284 在所有情况下都比较慢,尽管非常接近未使用unique_ptr的原始指针。当使用__restrict修饰符时,原始指针函数版本开始运行。 __restrict函数将忽略unique_ptr。如果指针别名但我的生产代码中很少(或没有)这样做,齿轮可能会磨碎。

结论:看来我将回顾代码的一些关键部分,以了解具有多个原始指针和唯一指针的函数。这种差异是可以忽略的方法。看起来在调用的函数中使用独特的ptrs get()方法以及使用__restrict原始指针非常有效。

版本VC ++ 15.9.2,编译器选项: / permissive- / 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:prompt / WX- / Zc:forScope / Gd / Oi / MD / std:c ++ 17 / FC / Fa” x64 \ Release \“ / EHsc / nologo / Fo“ x64 \ Release \” / diagnostics:classic

__restrict

2 个答案:

答案 0 :(得分:0)

我已经研究了MSVC如何在有别名和无别名的情况下进行优化,并包含了vector<int>版的测试函数f3()。集合现在是:

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是否指向相同的内存,如果是,则假定使用别名来产生缓慢但正确的代码。 / p>

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

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

总结一下,__restrict并没有真正加快原始ptr函数的速度,除非p1和p2指向相同的地址。当p1和p2不同时,它与unique_ptr和矢量版本一样快。

即使在调用原始指针函数时,编译器也会生成快速代码,而代价是通过在指针相等时假设别名来降低指针对等性的初始测试。

答案 1 :(得分:0)

  

是否有某种方法可以使用__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下进行代码编译,无论是否限制为相同的汇编指令,请参见here。如果希望加快执行速度,请开始使用特定于平台的说明。