Оболочка C ++ для C -API: Изучение лучших вариантов передачи `char *` - PullRequest
0 голосов
/ 24 января 2020

Было несколько вопросов по схожим темам , но я не нашел ни одного вопроса, который бы исследовал варианты таким образом.

Часто нам нужно обернуть устаревший C -API в C ++, чтобы использовать его очень хорошую функциональность, защищая нас от капризов. Здесь мы сосредоточимся только на одном элементе. Как обернуть устаревшие C -функции, которые принимают char* params. Пример спецификации c предназначен для API ( graphviz lib ), который принимает многие из его параметров как char* без указания, является ли это const или non-const. Похоже, что нет попыток изменить, но мы не можем быть уверены на 100%.

Вариант использования для оболочки - это то, что мы хотим удобно вызывать оболочку C ++ с различными «строковыми» именами и значениями свойств, поэтому строковые литералы, строки, константные строки, string_views и т. Д. c. Мы хотим вызвать как по одному во время установки , когда производительность не критична , так и во внутреннем l oop, 100M + раз, где производительность имеет значение. (Код сравнения внизу)

Многие способы передачи «строк» ​​в функции были объяснены в другом месте .

Приведенный ниже код тщательно прокомментирован для 4 опций функции cpp_wrapper(), вызываемой 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 '\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, поскольку работа в C -API минимальна и отсутствует в клиенте C ++. В полном приложении я знаю, что я могу сделать 10M в <1 с. Таким образом, простое переключение между этими двумя стилями абстракции API выглядит как изменение на 10%? Первые дни ... нужно больше работать. Примечание: это с короткой строкой, которая подходит для SBO. Более длинные с выделением кучи просто взорвать его полностью. </p>

#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

Ответы [ 2 ]

2 голосов
/ 24 января 2020

Сначала исправьте, затем, при необходимости, оптимизируйте.

  • wrapper1 имеет по крайней мере два потенциальных случая неопределенного поведения: сомнительный const_cast и (в отладочных версиях) возможно доступ к элементу после конец массива. (Вы можете создать указатель на один элемент после последнего, но не можете получить к нему доступ.)

  • wrapper2 также имеет сомнительный const_case, потенциально вызывающий неопределенное поведение.

  • wrapper3 не полагается на какой-либо UB (что я вижу).

  • wrapper4 похож на wrapper3, но предоставляет детали, которые вы пытаетесь инкапсулировать.

Начните с самого правильного, то есть скопируйте строки и передайте указатель на копию, которая является оберткой3.

Если производительность неприемлема в условиях ограниченного доступа. oop, вы можете посмотреть на альтернативы. Узкий l oop может использовать только подмножество интерфейсов. Узкий l oop может быть сильно смещен в сторону коротких или длинных струн. Компилятор может встроить достаточно вашей обертки в тесную l oop, чтобы он фактически не использовался. Эти факторы будут влиять на то, как (и если) вы решите проблему с производительностью.

Альтернативные решения могут включать кэширование, чтобы уменьшить количество сделанных копий, достаточно изучить базовую библиотеку, чтобы внести некоторые стратегические изменения c (например, изменение лежащей в основе библиотеки, чтобы использовать const, где это возможно), или путем перегрузки, которая выставляет char * и пропускает его напрямую (что перекладывает бремя на вызывающего, чтобы знать, что правильно).

Но все это деталь реализации: спроектируйте API для удобства использования вызывающими сторонами.

2 голосов
/ 24 января 2020

Завершено ли представление строки '\ 0'?

Если оно указывает на строку с нулевым завершением, то sv.data() может быть завершено нулем. Но представление строки не обязательно должно заканчиваться нулем, поэтому не следует предполагать, что это так. Таким образом, cpp_wrapper1 - плохой выбор.

Модифицирует ли старый API? .. мы не знаем.

Если вы не знаете, изменяет ли API строку, то вы не можете использовать const, поэтому cpp_wrapper2 не вариант.


Одна вещь, чтобы рассмотреть, является ли обертка необходима. Наиболее эффективное решение - передать char*, что прекрасно в C ++. Если использование константных строк является типичной операцией, тогда cpp_wrapper3 может быть полезным, но является ли это типичным, учитывая, что операции могут изменять строку? cpp_wrapper4 более эффективен, чем 3, но не так эффективен, как обычный char*, если у вас еще нет std::string.

Вы можете предоставить все перечисленные выше опции как перегрузки.

...