代码之家  ›  专栏  ›  技术社区  ›  Oliver Schönrock

C++ API的包装器:探索通过“char *”的最佳选项

  •  0
  • Oliver Schönrock  · 技术社区  · 6 年前

    已经发生了一些事件 questions on similar topics 但我发现没有一个能以这种方式探索选项。

    我们经常需要在C++中封装一个传统的C-API来使用它的非常好的功能,同时保护我们免受奇偶性的影响。这里我们只关注一个元素。如何包装接受 char* 帕玛斯。具体的例子是一个API( the graphviz lib )它接受许多参数作为 炭* 但没有具体说明 const non-const .似乎没有人试图修改,但我们不能100%确定。

    包装器的使用案例是,我们希望方便地调用具有各种“细长”属性名称和值的C++包装器,因此我们希望调用字符串文字、字符串、const字符串、String视图等。 都是在安装过程中单独进行的 性能不重要的地方 在内部循环中 ,超过100万次,性能确实很重要。(底部的基准代码)

    将“字符串”传递给函数的多种方式 have been explained elsewhere .

    下面的代码对 cpp_wrapper() 函数有5种不同的调用方式。

    哪个是最好/最安全/最快的选择?这是第二选择吗?

    #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 '\0' 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 '\0' 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
    // '\0' 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++客户端中不存在。在完整的应用程序中,我知道我可以在<1s。所以,仅仅在这两种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
    
    0 回复  |  直到 6 年前
        1
  •  2
  •   Adrian McCarthy    6 年前

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

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

    • wrapper2还有一个可疑的const_案例,可能会调用未定义的行为。

    • wrapper3不依赖任何UB(我看到)。

    • wrapper4与wrapper3类似,但会暴露您试图封装的细节。

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

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

    替代解决方案可能包括缓存以减少副本数量、充分调查底层库以进行一些战略性更改(如在可能的情况下将底层库更改为使用const),或者通过重载来公开副本 char * 并直接传递(这会将负担转移给打电话的人,让他们知道什么是正确的)。

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

        2
  •  2
  •   eerorika    6 年前

    字符串视图“\0”是否终止?

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

    遗留API是否会修改。。我们不知道。

    如果不知道API是否修改了字符串,那么就不能使用const,所以 cpp_wrapper2 这不是一个选项。


    需要考虑的是包装是否必要。最有效的解决办法是通过考试 char* 这在C++中是很好的。如果使用常量字符串是一种典型的操作,那么 cpp_wrapper3 可能有用——但考虑到操作可能会修改字符串,这是典型的吗? cpp_wrapper4 比3更有效,但没有普通的那么有效 炭* 如果你还没有 std::string .

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

    推荐文章