在没有未定义行为的情况下为双精度重用浮点缓冲区

Reusing a float buffer for doubles without undefined behaviour

在一个特定的 C++ 函数中,我碰巧有一个指针指向一个大的浮点数缓冲区,我想临时用它来存储一半的双精度数。有没有一种方法可以将此缓冲区用作临时 space 来存储双打,这也是标准允许的(即,不是未定义的行为)?

总而言之,我想要这样:

void f(float* buffer)
{
  double* d = reinterpret_cast<double*>(buffer);
  // make use of d
  d[i] = 1.;
  // done using d as scratch, start filling the buffer
  buffer[j] = 1.;
}

据我所知,没有简单的方法可以做到这一点:如果我理解正确,像这样的 reinterpret_cast<double*> 会由于类型别名和使用 memcpy 或 float/double union 如果不复制数据并分配额外的 space 是不可能的,这违背了目的并且在我的情况下恰好是昂贵的(并且在 C++ 中不允许使用联合进行类型双关) .

可以假设浮动缓冲区正​​确对齐以用于双打。

tl;dr 不要给指针起别名——根本不要——除非你在命令行上告诉编译器你要这样做。


执行此操作的最简单方法可能是找出禁用严格别名的编译器开关并将其用于相关源文件。

必须的,嗯?


再考虑一下。尽管有很多关于放置新东西的东西,但这是唯一安全的方法。

为什么?

好吧,如果您有两个不同类型的指针指向同一个地址,那么您就为该地址设置了别名,并且很有可能欺骗编译器。 并且您如何为这些指针赋值并不重要。编译器不会记住那个。

所以这是唯一安全的方法,这就是为什么我们需要 std::pun

我认为下面的代码是一个有效的方法(它实际上只是关于这个想法的一个小例子):

#include <memory>

void f(float* buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];

    // we have started the lifetime of the doubles.
    // "d" is a new pointer pointing to the first double object in the array.        
    // now you can use "d" as a double buffer for your calculations
    // you are not allowed to access any object through the "buffer" pointer anymore since the floats are "destroyed"       
    d[0] = 1.;
    // do some work here on/with the doubles...


    // conceptually we need to destory the doubles here... but they are trivially destructable

    // now we need to start the lifetime of the floats again
    new (buffer) float[10];  


    // here we are unsure about wether we need to update the "buffer" pointer to 
    // the one returned by the placement new of the floats
    // if it is nessessary, we could return the new float pointer or take the input pointer
    // by reference and update it directly in the function
}

int main()
{
    float* floats = new float[10];
    f(floats, sizeof(float) * 10);
    return 0;
}

重要的是您只使用从新放置收到的指针。并且重要的是将新的花车放回原处。即使是空构建,也需要重新开始float的lifetimes

忘记评论中的 std::launderreinterpret_cast。新的展示位置将为您完成这项工作。

编辑:确保在 main 中创建缓冲区时正确对齐。

更新:

我只是想更新评论中讨论的内容。

  1. 首先提到的是我们可能需要将初始创建的float指针更新为re-placement-new'ed floatreturned的指针(问题是初始float指针是否可以仍用于访问浮点数,因为浮点数现在是 "new" 通过附加新表达式获得的浮点数)。

为此,我们可以 a) 通过引用传递浮点指针并更新它,或者 b) return 从函数中获得的新浮点指针:

一)

void f(float*& buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];    
    // do some work here on/with the doubles...
    buffer = new (buffer) float[10];  
}

b)

float* f(float* buffer, std::size_t buffer_size_in_bytes)
{
    /* same as inital example... */
    return new (buffer) float[10];  
}

int main()
{
    float* floats = new float[10];
    floats = f(floats, sizeof(float) * 10);
    return 0;
}
  1. 接下来要提到的更重要的事情是placement-new允许有内存开销。因此允许该实现在 returned 数组的前面放置一些元数据。如果发生这种情况,那么天真地计算多少双打可以放入我们的记忆中显然是错误的。问题是,我们不知道实现会事先为特定调用获取多少字节。但这对于调整我们知道将适合剩余存储空间的双打数量是必要的。 这里 ( ) 是另一个 SO post,其中 Howard Hinnant 提供了一个测试片段。我使用在线编译器对此进行了测试,发现对于普通的可破坏类型(例如双精度),开销为 0。对于更复杂的类型(例如 std::string),开销为 8 个字节。但这可能因您的 plattform/compiler 而异。预先使用 Howard 的代码片段对其进行测试。

  2. 关于为什么我们需要使用某种放置 new(通过 new[] 或单个元素 new)的问题:我们被允许以我们想要的任何方式转换指针。但最后 - 当我们访问值时 - 我们需要使用正确的类型以避免违反严格的别名规则。简单的说:只有当指针给定的位置上确实存在指针类型的对象时,才允许访问对象。那么如何让物体栩栩如生呢? 标准说:

https://timsong-cpp.github.io/cppwp/intro.object#1 :

"An object is created by a definition, by a new-expression, when implicitly changing the active member of a union, or when a temporary object is created."

还有一个看起来很有趣的附加扇区:

https://timsong-cpp.github.io/cppwp/basic.life#1:

“如果一个对象是 class 或聚合类型,并且它或其子对象之一是由普通默认构造函数以外的构造函数初始化的,则称该对象具有非空初始化。生命周期T 类型对象的开始时间:

  • 获得具有适合类型 T 的对齐方式和大小的存储,并且
  • 如果对象有非空初始化,它的初始化完成

所以现在我们可能会争辩说,因为替身是琐碎的,我们是否需要采取一些行动来使琐碎的对象栩栩如生并改变实际的生活对象?我说是的,因为我们最初获得了浮点数的存储空间,通过双指针访问存储空间会违反严格的别名。所以我们需要告诉编译器实际类型已经改变。整个最后一点 3 的讨论非常有争议。您可以形成自己的意见。您现在掌握了所有信息。

您可以通过两种方式实现。

第一个:

void set(float *buffer, size_t index, double value) {
    memcpy(reinterpret_cast<char*>(buffer)+sizeof(double)*index, &value, sizeof(double));
}
double get(const float *buffer, size_t index) {
    double v;
    memcpy(&v, reinterpret_cast<const char*>(buffer)+sizeof(double)*index, sizeof(double));
    return v;
}
void f(float *buffer) {
    // here, use set and get functions
}

其次:代替float *,你需要分配一个"typeless" char[]缓冲区,并使用placement new将floats或double放在里面:

template <typename T>
void setType(char *buffer, size_t size) {
    for (size_t i=0; i<size/sizeof(T); i++) {
        new(buffer+i*sizeof(T)) T;
    }
}
// use it like this: setType<float>(buffer, sizeOfBuffer);

然后使用这个访问器:

template <typename T>
T &get(char *buffer, size_t index) {
    return *std::launder(reinterpret_cast<T *>(buffer+index*sizeof(T)));
}
// use it like this: get<float>(buffer, index) = 33.3f;

第三种方式可能类似于 phön 的回答(请参阅我在该回答下的评论),不幸的是,由于 this problem.

,我无法找到合适的解决方案

此问题无法在可移植的 C++ 中解决。

C++ 在指针别名方面非常严格。有点矛盾的是,这允许它在很多平台上编译(例如,可能 double 数字存储在与 float 数字不同的地方)。

不用说,如果您正在努力实现可移植代码,那么您将需要重新编码您拥有的代码。第二个最好的事情是务实,接受它可以在我遇到的任何桌面系统上运行;甚至 static_assert 编译器名称/体系结构。

这是一种不那么可怕的替代方法。

你说,

...a float/double union is not possible without...allocating extra space, which defeats the purpose and happens to be costly in my case...

所以让每个联合对象包含两个浮点数而不是一个。

static_assert(sizeof(double) == sizeof(float)*2, "Assuming exactly two floats fit in a double.");
union double_or_floats
{
    double d;
    float f[2];
};

void f(double_or_floats* buffer)
{
    // Use buffer of doubles as scratch space.
    buffer[0].d = 1.0;
    // Done with the scratch space.  Start filling the buffer with floats.
    buffer[0].f[0] = 1.0f;
    buffer[0].f[1] = 2.0f;
}

当然这样索引会比较复杂,调用代码也得修改。但它没有开销,而且更明显是正确的。