【问题标题】:Calling FFTW's in-place real-to-complex transform without violating strict aliasing rules在不违反严格的混叠规则的情况下调用 FFTW 的就地实数到复数变换
【发布时间】:2020-01-05 01:31:13
【问题描述】:

我想调用fftw's in-place real-to-complex transform函数,签名如下:

fftw_plan fftw_plan_dft_r2c_1d(
    int n,             // transform length
    double* in,        // pointer to input array
    fftw_complex* out, // pointer to output array
    unsigned flags     // flags
);

文档说我应该通过为inout 参数传递别名指针来表明我希望执行就地转换。


问题:inout 如何在不违反严格的别名规则的情况下使用别名?


我对 GCC 特定的扩展持开放态度(即,使用unions 进行类型双关,即使标准声明这是未定义的行为)。即使允许此扩展,联合也不能包含动态大小的数组(这在此应用程序中是必须的——我事先不知道变换长度)。有没有人有任何想法?提前致谢。

【问题讨论】:

  • 您从哪里得到有问题的代码没有违反严格别名规则的印象?
  • @Nicol Bolas:我想我的问题的改写可能是:FFTW 的接口是否强制调用者调用未定义的行为?或者有没有办法解决它(除了用-fno-strict-aliasing编译)?
  • 不幸的是,严格的别名规则没有官方解决方法。语言在这方面被打破了;没有办法(目前)合法地将类型别名从 UB 领域获取到实现定义的领域,缺少内联汇编或您提到的编译器特定开关。

标签: c++ gcc c++17 fftw strict-aliasing


【解决方案1】:

我将挑战这个前提:不要过于担心严格的别名。

创建一个double 的数组并将指向它的指针传递给inreinterpret_cast 指向fftw_complex * 的指针并将其传递给out

从此数组中读取结果doubles(作为复数的实部和虚部对)。


是的,fftw_plan_dft_r2c_1d 如果以这种方式调用,可能会在后台破坏严格的别名。

但由于它位于单独的翻译单元中,并且调用者没有违反严格别名,您的编译器无法判断是否确实违反了严格别名。

fftw_complexessentiallystruct fftw_complex {double re, im;};,所以一切正常。

为了额外的安全,您可以添加:

static_assert(sizeof(fftw_complex) == 2 * sizeof(double) && alignof(fftw_complex) <= alignof(double));

【讨论】:

  • fftw 的 UB 确实隐藏在另一个翻译单元中,但是调用者要对库进行任何实际使用,他们必须写入 in 缓冲区,然后从out 缓冲区 - 因此违反了规则,可能在单个函数中。在这种情况下,担心编译器优化会破坏代码似乎是合理的。在我的应用程序中,情况更糟——代码是类模板的一部分,因此编译器可以看到更多的代码,包括我没有编写的代码,从而为编译器提供了更多的破解代码的机会。
  • @SumDood 不,调用者不会破坏严格的别名。它只适用于doubles 的数组。在这种情况下,调用者所做的唯一值得怀疑的事情是reinterpret_cast,但这个演员表本身不会导致 UB。 (从编译器的角度来看,fftw_complex 理论上可以将输出指针转换回double * 并写入这个新指针,因此不会产生严格的别名。)
  • 调用者确实打破了严格的别名,因为调用者必须填充double的数组(通过指向double的指针),调用fftw_execute,然后读取fftw_complex 的数组(通过指向fftw_complex 的指针)。因为这两个指针实际上指向同一个内存,所以违反了严格的别名规则。任何违反严格别名规则的使用方案要么不填充输入数组,要么不读取输出数组。调用方的任何实际使用都将填充输入数组读取输出数组(通过指向不同类型的指针)。
  • @SumDood 正如答案所说,我建议从数组中读取fftw_complex。我建议从中读取doubles(成对,其中array[i*2] 是实部,array[i*2+1] 是这些复数的虚部)。
  • 啊,我明白了。是的,假设没有链接时间优化,这可能是安全的。
【解决方案2】:

根据this linkfftw_complex是以下typedef

typedef double fftw_complex[2];

根据 C++20 之前的规则,fftw_complex* 可能因此而别名 double* ([basic.lval]p8.6):

如果一个程序试图通过 Glvalue 访问一个对象的存储值,而不是其中一个 以下类型的行为未定义:
...
聚合 或联合类型,包括上述之一 其元素或非静态数据成员之间的类型(包括, 递归地,子聚合的元素或非静态数据成员或 包含联合)

数组是一个聚合,我们的数组包含doubles,因此它可以给double指针起别名。因此,fftw_plan_dft_r2c_1d 函数中不会发生严格的别名规则违规,您可以安全地使用它。

但是,请注意,这一段已从 C++20 标准中删除,并且有争议的是它也应从 C 标准中删除。但是由于它还没有被删除并且 GCC & clang 实际上尊重它,我想可以安全地假设行为不会随着 C++20 实现而改变。据我所知,MSVC 根本没有利用 SAR。

【讨论】:

  • 呵呵,this linkfftw_complex 提供了不同的定义。
  • @HolyBlackCat,这就是为什么我要问它到底是什么。但我去了他们的网站,只使用了最新的文档版本。无论如何,您的链接包含一个同样有效的结构。因为标准中的相同段落。
  • 您(或其他任何人)是否知道将其从标准中删除的理由?
  • @SumDood 它被认为是多余的。你看,我的帖子中描述的内容(你的问题中构造的合法性)看起来像是完全不同意图的副产品。上述段落允许别名,但似乎它的创建不是允许它,而是允许在 C 中进行结构分配。也就是说,这一段阻止了语言时尚的积极编译器优化,所以你可以用语言规则来推理为什么它不能UB。但你应该明白,SAR 是一个有争议的话题,它产生 UB 是有原因的。
  • 原因是优化。并且一些编译器会尽可能地利用它,但是这段代码的参数在任何情况下都会阻止优化:这一段是否存在都没有关系。编译器不能利用这种别名,并且他们不能别名从而产生一个 UB。问题是in 将始终能够别名out,因为后者由双精度数组成,并且双精度数可以在任何一天使用双精度数,这在 C++20 中不会改变。所以,我想说代码无论如何都会保持正确,但推理会变得有点模糊。
【解决方案3】:

这种结构并不一定意味着别名违规。 在fftw_plan_dft_r2c_1d 内部,可能有一个placement new 数组调用,它会正确创建out 缓冲区。

对于 C++17,您可能希望在调用后将 std::launder 指向 out 指针,以完全符合标准。

【讨论】:

    猜你喜欢
    • 2011-04-15
    • 1970-01-01
    • 1970-01-01
    • 2015-10-04
    • 1970-01-01
    • 2015-11-06
    • 1970-01-01
    • 1970-01-01
    • 2018-05-23
    相关资源
    最近更新 更多