C++ C-API 的包装器:探索传递"char*"的最佳选项



有很多关于类似主题的问题,但我发现没有一个以这种方式探索选项的问题。

通常,我们需要将遗留的C-API包装在C++中,以使用它非常好的功能,同时保护我们免受变幻莫测的影响。在这里,我们将只关注一个元素。如何包装接受char*参数的旧 C 函数。具体的例子是一个API(graphviz lib(,它接受它的许多参数作为char*,而不指定这是const还是non-const。似乎没有尝试修改,但我们不能 100% 确定。

包装器的用例是,我们希望方便地使用各种"字符串"属性名称和值来调用C++包装器,因此字符串文字、字符串、常量字符串、string_views等。我们希望在性能非关键设置期间单独调用,在性能确实很重要的内部循环中调用 100M+ 次。(底部的基准代码(

将"字符串"传递给函数的许多方法已在别处解释过。

下面的代码被大量注释了cpp_wrapper()函数的 4 个选项,以 5 种不同的方式调用。

哪个是最好/最安全/最快的选择?是选择2的情况吗?

#include <array>
#include <cassert>
#include <cstdio>
#include <string>
#include <string_view>
void legacy_c_api(char* s) {
// just for demo, we don't really know what's here.
// specifically we are not 100% sure if the code attempts to write
// to char*. It seems not, but the API is not `const char*` eventhough C
// supports that
std::puts(s);
}
// the "modern but hairy" option
void cpp_wrapper1(std::string_view sv) {
// 1. nasty const_cast. Does the legacy API modifY? It appears not but we
// don't know.
// 2. Is the string view '' terminated? our wrapper api can't tell
// so maybe an "assert" for debug build checks? nasty too?!
// our use cases below are all fine, but the API is "not safe": UB?!
assert((int)*(sv.data() + sv.size()) == 0);
legacy_c_api(const_cast<char*>(sv.data()));
}
void cpp_wrapper2(const std::string& str) {
// 1. nasty const_cast. Does the legacy API modifY? It appears not but we
//    don't know. note that using .data() would not save the const_cast if the
//    string is const
// 2. The standard says this is safe and null terminated std::string.c_str();
//    we can pass a string literal but we can't pass a string_view to it =>
//    logical!
legacy_c_api(const_cast<char*>(str.c_str()));
}
void cpp_wrapper3(std::string_view sv) {
// the slow and safe way. Guaranteed be '' terminated.
// is non-const so the legacy can modfify if it wishes => no const_cast
// slow copy?  not necessarily if sv.size() < 16bytes => SBO on stack
auto str = std::string{sv};
legacy_c_api(str.data());
}
void cpp_wrapper4(std::string& str) {
// efficient api by making the proper strings in calling code
// but communicates the wrong thing altogether => effectively leaks the c-api
// to c++
legacy_c_api(str.data());
}
// std::array<std::string_view, N> is a good modern way to "store" a large array
// of "stringy" constants? they end up in .text of elf file (or equiv). They ARE
// '' terminated. Although the sv loses that info. Used in inner loop => 100M+
// lookups and calls to legacy_c_api;
static constexpr const auto sv_colours =
std::array<std::string_view, 3>{"color0", "color1", "color2"};
// instantiating these non-const strings seems wrong / a waste (there are about
// 500 small constants) potenial heap allocation in during static storage init?
// => exceptions cannot be caught... just the wrong model?
static auto str_colours =
std::array<std::string, 3>{"color0", "color1", "color2"};
int main() {
auto my_sv_colour  = std::string_view{"my_sv_colour"};
auto my_str_colour = std::string{"my_str_colour"};
cpp_wrapper1(my_sv_colour);
cpp_wrapper1(my_str_colour);
cpp_wrapper1("literal_colour");
cpp_wrapper1(sv_colours[1]);
cpp_wrapper1(str_colours[2]);
// cpp_wrapper2(my_sv_colour); // compile error
cpp_wrapper2(my_str_colour);
cpp_wrapper2("literal_colour");
// cpp_wrapper2(colours[1]); // compile error
cpp_wrapper2(str_colours[2]);
cpp_wrapper3(my_sv_colour);
cpp_wrapper3(my_str_colour);
cpp_wrapper3("literal_colour");
cpp_wrapper3(sv_colours[1]);
cpp_wrapper3(str_colours[2]);
// cpp_wrapper4(my_sv_colour);  // compile error
cpp_wrapper4(my_str_colour);
// cpp_wrapper4("literal_colour"); // compile error
// cpp_wrapper4(sv_colours[1]); // compile error
cpp_wrapper4(str_colours[2]);
}

基准代码

还不完全现实,因为 C-API 中的工作很少,并且在客户端中不存在C++。在完整的应用程序中,我知道我可以在 <1 秒内完成 10M。因此,仅在这 2 种 API 抽象样式之间切换看起来可能是 10% 的变化?初期。。。需要更多的工作。 注意:这是使用适合 SBO 的短字符串。带有堆分配的较长版本只是将其完全吹出。

#include <benchmark/benchmark.h>
static void do_not_optimize_away(void* p) {
asm volatile("" : : "g"(p) : "memory");
}
void legacy_c_api(char* s) {
// do at least something with the string
auto sum = std::accumulate(s, s+6, 0);
do_not_optimize_away(&sum);
}
// ... wrapper functions as above: I focused on 1&3 which seem 
// "the best compromise". 
// Then I added wrapper4 because there is an opportunity to use a 
// different signature when in main app's tight loop. 
void bench_cpp_wrapper1(benchmark::State& state) {
for (auto _: state) {
for (int i = 0; i< 100'000'000; ++i) cpp_wrapper1(sv_colours[1]);
}
}
BENCHMARK(bench_cpp_wrapper1);
void bench_cpp_wrapper3(benchmark::State& state) {
for (auto _: state) {
for (int i = 0; i< 100'000'000; ++i) cpp_wrapper3(sv_colours[1]);
}
}
BENCHMARK(bench_cpp_wrapper3);
void bench_cpp_wrapper4(benchmark::State& state) {
auto colour = std::string{"color1"};
for (auto _: state) {
for (int i = 0; i< 100'000'000; ++i) cpp_wrapper4(colour);
}
}
BENCHMARK(bench_cpp_wrapper4);

结果

-------------------------------------------------------------
Benchmark                   Time             CPU   Iterations
-------------------------------------------------------------
bench_cpp_wrapper1   58281636 ns     58264637 ns           11
bench_cpp_wrapper3  811620281 ns    811632488 ns            1
bench_cpp_wrapper4  147299439 ns    147300931 ns            5

首先更正,然后根据需要进行优化。

  • wrapper1 至少有两个未定义行为的潜在实例:可疑的const_cast,以及(在调试版本中(可能访问数组末尾的元素。 (您可以创建指向最后一个元素之后的一个元素的指针,但无法访问它。

  • Wrapper2 也有一个可疑const_case,可能会调用未定义的行为。

  • 包装器3不依赖于任何UB(我看到的(。

  • Wrapper4 与 Wrapper3 类似,但会公开您尝试封装的细节。

首先做最正确的事情,即复制字符串并传递指向副本的指针,即 wrapper3。

如果性能在紧密循环中不可接受,您可以查看替代方案。 紧密循环可能仅使用接口的子集。 紧密循环可能严重偏向于短弦或长弦。 编译器可能会在紧密循环中内联足够多的包装器,以至于它实际上是一个无操作。 这些因素将影响您解决性能问题的方式(以及是否(。

替代解决方案可能涉及缓存以减少制作的副本数,调查基础库足以进行一些战略更改(例如更改基础库以尽可能使用 const(,或者通过重载来公开char *并直接传递它(这会将负担转移给调用方以了解什么是正确的(。

但所有这些都是实现细节:为调用方的可用性设计 API。

字符串

视图 '\0' 是否终止?

如果它恰好指向以 null 结尾的字符串,则sv.data()可能是以 null 结尾的。但是字符串视图不需要以 null 结尾,因此不应假设它是。因此,cpp_wrapper1是一个糟糕的选择。

遗留的API是否修改?..我们不知道。

如果您不知道 API 是否修改了字符串,那么您不能使用 const,因此cpp_wrapper2不是一个选项。


要考虑的一件事是是否需要包装器。最有效的解决方案是通过char*,这在C++中很好。如果使用 const 字符串是典型的操作,那么cpp_wrapper3可能很有用 - 但考虑到操作可能会修改字符串,这是典型的吗?cpp_wrapper4比 3 更有效率,但如果您还没有std::string,则不如普通char*效率高。

您可以将上述所有选项作为重载提供。

最新更新