C++ 使用 constexpr 使放置新对齐存储可初始化
C++ make placement new aligned storage initializable using constexpr
这对我来说是一个非常重要的问题,因为它现在是一个瓶颈,我正在尝试研究解决我的问题的可能方法:我需要 constexpr 构造一个 std::function-like class 我用的那个很简单。但是,它使用对齐存储,因此我们可以配置指针大小的捕获元素数。我们称它为函数。
https://github.com/fwsGonzo/libriscv/blob/master/lib/libriscv/util/function.hpp#L91
具体来说,我正在使用最多捕获 1 个指针的函数。通常是“这个”。这些函数运行良好,如果您尝试捕获太多,它们将无法编译。
问题是它们必须在 运行 时间构建,而且它们的数量太多以至于它们使用了大约 3500 纳秒(3.5 微秒),这对我的用例来说是永恒的.我绝对必须找到一种方法来以某种方式降低此设置成本,因此自然的方法是调查我是否可以在编译时构建它们。
我无法这样做,编译器直接告诉我使用 placement new 的构造函数不能在 constexpr 上下文中使用。这个问题讲述了同样的故事:
C++ constexpr in place aligned storage construction
您可以在此处查看有问题的语句:
https://github.com/fwsGonzo/libriscv/blob/master/lib/libriscv/util/function.hpp#L148
template<typename Callable>
Function (Callable callable) noexcept
{
static_assert(sizeof(Callable) <= FunctionStorageSize,
"Callable too large (greater than FunctionStorageSize)");
static_assert(std::is_trivially_copy_constructible_v<Callable>,
"Callable not trivially copy constructible");
static_assert(std::is_trivially_destructible_v<Callable>,
"Callable not trivially destructible");
m_func_ptr = &trampoline<Callable>;
new(reinterpret_cast<Callable *>(m_storage.data)) Callable(callable);
}
我正在使用 C++20,我愿意接受有关如何解决此问题的建议。鉴于这些函数有一个固定大小的捕获存储和一个函数指针,是否有可能在编译时以某种方式构造它们?不应由此产生堆分配。
使用 C++20,如果您将类型 Callable
的约束增加为 trivially_copyable
,您可以使用 bit_cast
。您还必须为所有可能的对象大小定义一个包含 aligned_storage <size, alignment>
类型成员的联合。
不幸的是,我认为还没有 bit_cast
的 constexpr 实现。
如果 Callable
指定指向对象类型的指针,则部分解决方案可能是声明一个 constexpr 构造函数:
template<typename Callable>
constexpr
Function (Callable * callable) noexcept
m_pointer {callable}
m_func_ptr = &trampoline <Callable>
{}
//declare the union
union {
void * m_pointer;
Storage m_storage;
};
//end an overload trampoline specialized for pointer to object.
虽然 C++20 确实允许您在 constexpr 上下文中动态分配内存,但在编译时分配的内存不允许泄漏到运行时执行中。因此 constexpr 分配必须静态绑定到常量表达式求值。
即使有 C++20 的特性,你也不能在编译时使用 placement new。
我最终找到了解决这个问题的方法。我的大多数类函数对象只是围绕此函数 class 的原始函数指针,因此我最终尝试使这部分成为 constexpr,并取得了成功。这不是其他人可以回答的问题,因为当您写问题时您无法想到所有内容,而我最终获得了更多信息。尽管如此,对于将来尝试这样做的任何人:您可能无法制作 lambda-with-capture constexpr,但您仍然可以像我所做的那样,如下所示。
通过添加一个与原始函数指针相匹配的新类型,然后像这样在实例化模板中捕获它:
template <>
constexpr Function<RawFunctionPointerType>(RawFunctionPointerType fptr) noexcept
: m_func_ptr(&trampoline<RawFunctionPointerType>), m_real_ptr{fptr} {}
m_real_ptr 成员与存储联合:
union {
RawFunctionPointerType m_real_ptr;
Storage m_storage;
};
可以 constinit 实例化一个 std::array,它可以在运行时 std::copy 进入我的结构。通过这样做,我最终节省了至少 1 微秒。
我也创建了自己的类型擦除函数。它不是 constexpr,因为我需要使用 placement new 或 std::memcopy 来填充我的存储空间。
主要思想是为“蹦床生成”使用非捕获 lambda,也许您可以使用它。
优化后生成的程序集在我眼中看起来非常好... godbolt
#include <iostream>
#include <cstring>
namespace Test
{
template<typename Return, typename... Args>
using InvokeFktPtr = Return(*)(const void*, Args...);
template <
typename Fkt
>
class SingleCastDelegate;
template <
typename ReturnType,
typename... Args
>
class SingleCastDelegate<ReturnType(Args...)>
{
private:
InvokeFktPtr<ReturnType, Args...> invokeFktPtr;
private:
static constexpr size_t max_lambda_size = 4 * sizeof(void*);
std::byte storage[max_lambda_size];
private:
constexpr const void* GetData() const
{
return std::addressof(storage[0]);
}
constexpr void* GetData()
{
return std::addressof(storage[0]);
}
public:
template<
typename Lambda
,typename PureLambda = std::remove_reference_t<Lambda>
>
inline SingleCastDelegate(Lambda&& lambda)
{
constexpr auto lambdaSize = sizeof(PureLambda);
static_assert(lambdaSize <= sizeof(void*) * 4);
//add some static_asserts... (it must be trivial...)
//placement new is not constexpr, or?
new(std::addressof(storage)) PureLambda(lambda);
invokeFktPtr = [](const void* data, Args... args)
{
const PureLambda& l = *static_cast<const PureLambda*>(data);
return l(args...);
};
}
template<
typename... CustomArgs
>
using FktPtr = ReturnType(*)(CustomArgs...);
template<
typename... CustomArgs
, typename = typename std::enable_if_t<std::is_invocable_v<FktPtr<Args...>, CustomArgs...>>
>
constexpr ReturnType operator()(CustomArgs&&... args) const
{
return invokeFktPtr(GetData(), std::forward<CustomArgs>(args)...);
}
};
}
int main()
{
int i = 42;
auto myFkt = [=](){
std::cout << i;
};
auto myOtherFkt = [=](){
std::cout << i * 2;
};
Test::SingleCastDelegate<void()> fkt = Test::SingleCastDelegate<void()>{ myFkt };
fkt();
fkt = myOtherFkt;
fkt();
return 0;
}
这对我来说是一个非常重要的问题,因为它现在是一个瓶颈,我正在尝试研究解决我的问题的可能方法:我需要 constexpr 构造一个 std::function-like class 我用的那个很简单。但是,它使用对齐存储,因此我们可以配置指针大小的捕获元素数。我们称它为函数。
https://github.com/fwsGonzo/libriscv/blob/master/lib/libriscv/util/function.hpp#L91
具体来说,我正在使用最多捕获 1 个指针的函数。通常是“这个”。这些函数运行良好,如果您尝试捕获太多,它们将无法编译。
问题是它们必须在 运行 时间构建,而且它们的数量太多以至于它们使用了大约 3500 纳秒(3.5 微秒),这对我的用例来说是永恒的.我绝对必须找到一种方法来以某种方式降低此设置成本,因此自然的方法是调查我是否可以在编译时构建它们。
我无法这样做,编译器直接告诉我使用 placement new 的构造函数不能在 constexpr 上下文中使用。这个问题讲述了同样的故事:
C++ constexpr in place aligned storage construction
您可以在此处查看有问题的语句: https://github.com/fwsGonzo/libriscv/blob/master/lib/libriscv/util/function.hpp#L148
template<typename Callable>
Function (Callable callable) noexcept
{
static_assert(sizeof(Callable) <= FunctionStorageSize,
"Callable too large (greater than FunctionStorageSize)");
static_assert(std::is_trivially_copy_constructible_v<Callable>,
"Callable not trivially copy constructible");
static_assert(std::is_trivially_destructible_v<Callable>,
"Callable not trivially destructible");
m_func_ptr = &trampoline<Callable>;
new(reinterpret_cast<Callable *>(m_storage.data)) Callable(callable);
}
我正在使用 C++20,我愿意接受有关如何解决此问题的建议。鉴于这些函数有一个固定大小的捕获存储和一个函数指针,是否有可能在编译时以某种方式构造它们?不应由此产生堆分配。
使用 C++20,如果您将类型 Callable
的约束增加为 trivially_copyable
,您可以使用 bit_cast
。您还必须为所有可能的对象大小定义一个包含 aligned_storage <size, alignment>
类型成员的联合。
不幸的是,我认为还没有 bit_cast
的 constexpr 实现。
如果 Callable
指定指向对象类型的指针,则部分解决方案可能是声明一个 constexpr 构造函数:
template<typename Callable>
constexpr
Function (Callable * callable) noexcept
m_pointer {callable}
m_func_ptr = &trampoline <Callable>
{}
//declare the union
union {
void * m_pointer;
Storage m_storage;
};
//end an overload trampoline specialized for pointer to object.
虽然 C++20 确实允许您在 constexpr 上下文中动态分配内存,但在编译时分配的内存不允许泄漏到运行时执行中。因此 constexpr 分配必须静态绑定到常量表达式求值。
即使有 C++20 的特性,你也不能在编译时使用 placement new。
我最终找到了解决这个问题的方法。我的大多数类函数对象只是围绕此函数 class 的原始函数指针,因此我最终尝试使这部分成为 constexpr,并取得了成功。这不是其他人可以回答的问题,因为当您写问题时您无法想到所有内容,而我最终获得了更多信息。尽管如此,对于将来尝试这样做的任何人:您可能无法制作 lambda-with-capture constexpr,但您仍然可以像我所做的那样,如下所示。
通过添加一个与原始函数指针相匹配的新类型,然后像这样在实例化模板中捕获它:
template <>
constexpr Function<RawFunctionPointerType>(RawFunctionPointerType fptr) noexcept
: m_func_ptr(&trampoline<RawFunctionPointerType>), m_real_ptr{fptr} {}
m_real_ptr 成员与存储联合:
union {
RawFunctionPointerType m_real_ptr;
Storage m_storage;
};
可以 constinit 实例化一个 std::array,它可以在运行时 std::copy 进入我的结构。通过这样做,我最终节省了至少 1 微秒。
我也创建了自己的类型擦除函数。它不是 constexpr,因为我需要使用 placement new 或 std::memcopy 来填充我的存储空间。
主要思想是为“蹦床生成”使用非捕获 lambda,也许您可以使用它。 优化后生成的程序集在我眼中看起来非常好... godbolt
#include <iostream>
#include <cstring>
namespace Test
{
template<typename Return, typename... Args>
using InvokeFktPtr = Return(*)(const void*, Args...);
template <
typename Fkt
>
class SingleCastDelegate;
template <
typename ReturnType,
typename... Args
>
class SingleCastDelegate<ReturnType(Args...)>
{
private:
InvokeFktPtr<ReturnType, Args...> invokeFktPtr;
private:
static constexpr size_t max_lambda_size = 4 * sizeof(void*);
std::byte storage[max_lambda_size];
private:
constexpr const void* GetData() const
{
return std::addressof(storage[0]);
}
constexpr void* GetData()
{
return std::addressof(storage[0]);
}
public:
template<
typename Lambda
,typename PureLambda = std::remove_reference_t<Lambda>
>
inline SingleCastDelegate(Lambda&& lambda)
{
constexpr auto lambdaSize = sizeof(PureLambda);
static_assert(lambdaSize <= sizeof(void*) * 4);
//add some static_asserts... (it must be trivial...)
//placement new is not constexpr, or?
new(std::addressof(storage)) PureLambda(lambda);
invokeFktPtr = [](const void* data, Args... args)
{
const PureLambda& l = *static_cast<const PureLambda*>(data);
return l(args...);
};
}
template<
typename... CustomArgs
>
using FktPtr = ReturnType(*)(CustomArgs...);
template<
typename... CustomArgs
, typename = typename std::enable_if_t<std::is_invocable_v<FktPtr<Args...>, CustomArgs...>>
>
constexpr ReturnType operator()(CustomArgs&&... args) const
{
return invokeFktPtr(GetData(), std::forward<CustomArgs>(args)...);
}
};
}
int main()
{
int i = 42;
auto myFkt = [=](){
std::cout << i;
};
auto myOtherFkt = [=](){
std::cout << i * 2;
};
Test::SingleCastDelegate<void()> fkt = Test::SingleCastDelegate<void()>{ myFkt };
fkt();
fkt = myOtherFkt;
fkt();
return 0;
}