优化友好的矢量转换/转换

问题描述 投票:0回答:2

我不相信这个问题有一个好的答案。大多数答案都建议范围初始化

std::transform
。有时建议“依靠 SmartCompiler™ 来了解您想要什么并执行必要的优化”

但是,我不相信有一种方法可以实现

vector
与标准布局 Pod 结构完全允许的相同:

struct S1 { int a; } s1;
int* p1 = reinterpret_cast<int*>(&s1); // value of p1 is "pointer to s1.a" because
                                       // s1.a and s1 are pointer-interconvertible

我认为这也代表动态数组

auto s1 = new S1[n];

有很多类似的问题需要类似的转换(可能转换为相同大小和布局的基类型指针)。

我确实相信这样的类型转换在像C++这样的低级语言中应该是可能的。但是,我不知道如何使用标准库来完成它,并且不会得到不必要的重新分配和复制。

这是一个简单的代码,导致汇编代码非常臃肿:
https://godbolt.org/z/34Grvb9Gj

#include <cstddef>
#include <vector>

#include <algorithm>
#include <numeric>

struct Int
{
    int value;
};

static std::vector<Int> init(size_t size){
    std::vector<Int> res;
    res.reserve(size);
    for (size_t i = 0; i < size; ++i)
        res.push_back(Int{int(i)});

    return res;
}

int stdTransform(size_t size){

    // const size_t size = 815;
    auto wrapped = init(size);

    std::vector<int> out;
    out.reserve(size);

    std::transform(std::move_iterator(wrapped.begin()), std::move_iterator(wrapped.end()),
        std::back_inserter(out), 
        [](Int&& v) {
            const int r = v.value;
            v.value = {};
            return r;
        });
    
    return std::accumulate(out.cbegin(), out.cend(), 0);
}

我可以看到有 2 个电话打给

operator new
。一般来说,我不期望简单的类型转换(在这种情况下应该会产生
zero
指令)会生成那么多带有
-O3
的指令。

更重要的是,如果我删除输入参数

size
(
void stdTransform()
) 并将初始化硬编码为
const size_t size = 815;
,仍然有很多指令。
由于在编译时已知
size
,我希望所有内容都应在编译时评估,就像使用
std::array
而不是
std::vector
一样。

考虑到这一背景,问题很简单:

  1. 有没有办法在不重新分配的情况下投射
    std::vector
  2. 当 Pod 类型的构造/销毁显然没有副作用时,为什么编译器无法优化转换? (而且
    bad_alloc
    这里没有任何意义,因为我们没有任何重新分配的意图)。
c++ c++17 compiler-optimization
2个回答
1
投票

有没有办法在不重新分配的情况下强制转换 std::vector ?

如果您想在保持原始向量处于活动状态的同时对数据进行转换和处理,一种标准方法是使用跨度:

int cast_using_span(size_t size) {
    auto wrapped = init(size);
    std::span<int> out = std::span<int>(reinterpret_cast<int*>(wrapped.data()), wrapped.size());
    return std::accumulate(out.begin(), out.end(), 0);
}

span<T>
在很多方面与
const vector<T>&
兼容,但您可能需要调整下游代码才能使用它。

请注意,这种转换仅在结构体中没有填充时才起作用,即

sizeof(Int) == sizeof(int)
,这对于
int
通常是正确的,但例如默认情况下对于
bool
则不是这样。

话虽如此,有一种 UB 方式可能在某些情况下有效:

int cast_using_UB(size_t size) {
    auto wrapped = init(size);
    std::vector<int>* out = reinterpret_cast<std::vector<int>*>(&wrapped);
    return std::accumulate(out->cbegin(), out->cend(), 0);
}

这假设

vector<int>
没有专门化,并且
vector<Int>
vector<int>
具有相同的内存布局。

如果您想将底层缓冲区的所有权转换并移动到新的向量实例,不幸的是,这对于 stdlib 来说是不可能的。它没有实现的部分原因是自定义分配器使事情变得复杂。我认为这是一个可以解决的问题,并且它可能是对支持此用例的标准的有用补充。

另请参阅 std::string 的相关问题:https://www.reddit.com/r/cpp_questions/comments/vcjk35/stdstring_to_byte_buffer_without_copy/

当 Pod 类型的构造/销毁显然没有副作用时,为什么编译器无法优化转换?

因为代码在 2 个独立的向量实例上运行,这些实例有 2 个独立的数据缓冲区。

std::move_iterator
没有任何效果,因为
struct Int
是一个普通的值类型。即使不是,您也不能以这种方式移动
wrapped.data()
缓冲区。唯一支持的移动方式(在当前的 stdlib 中)是移动到与源向量相同
vector<T>
类型的另一个实例,不允许进行强制转换。

在编译时已知大小的情况下,我希望所有内容都应该在编译时进行评估

这有点过于乐观了:) 以你写的特殊方式

res.reserve(815)
没有任何魔法可以做到这一点。它只是调用默认分配器。

理论上,如果我们用

std::vector<Int> res{815};
创建一个向量,我们可以想象未来 C++35 的 stdlib 会更聪明,constexpr 一切,并通过
std::array<Int, 55>
在内部支持它。

实现此类行为(创建静态分配的 std::vector)的当前方法是使用自定义分配器。例如,请参阅:Hinnant 的 Short_alloc 和对齐保证

TLDR;如果无法保留原始向量,C++ std::vector 和 std::string 不允许在没有副本的情况下进行“强制转换”,否则可以使用非拥有的跨度/视图/指针。此问题的具体解决方案是使用自定义类型而不是 std::vector,或者使所有下游代码通用以接受任何集合,例如 std::vector、std::span、range 等。


0
投票

如果您可以访问 C++20,则可以使用 std::ranges::transform_view:

int int_transform_view_accumulate(){

    auto wrapped = init();

    auto out = wrapped | std::views::transform([](Int& i) -> int& {return reinterpret_cast<int&>(i);});

    return std::accumulate(out.begin(), out.end(), 0);

}

https://godbolt.org/z/dcc83ce5j new 永远不会被调用(我没有定义 init 来隔离示例,当然其中会调用 new )。

通过单独铸造每个元素,可以避免 UB。如果您仅限于 C++17,则可以使用 Ranges-v3 库。

© www.soinside.com 2019 - 2024. All rights reserved.