Кирилл Березин
Ведущий программист
Как не сделать
врагами архитектуру
и оптимизацию
14 Ноября 2019
Постановка задачи
● Задача – сменить библиотеку json;
● Json использовался в качестве внутреннего
формата данных;
● Использование шаблонов выражений для
уменьшения кода.
Зачем все это было?
● Исследование показало, что операции с
json занимали не меньше 20 % времени;
● Rapidjson read rate 140 МБ/с
(худший);
● Cpprest sdk 13 МБ/с.
Архитектура программы
l Функциональность
в библиотеках;
l Внутренний формат
данных json-lib.
dll
Json
wrapper
dll
exe
Первые результаты
l Результат перевода теста одной из
библиотек показал:
- Rapidjson 15-25 МБ/с
- Cpp rest sdk 13 МБ/с
Исследуем код cpprest
typedef std::vector<std::pair<utility::string_t, json::value>> storage_type;
Исследуем код cpprest
typedef std::vector<std::pair<utility::string_t, json::value>> storage_type;
Исследуем код cpprest
l Используется move семантика;
l Элементы могут быть сортированы –
бинарный поиск;
l Элементы полиморфные;
l Поставляется в виде отдельной
библиотеки.
Исследуем код Rapidjson
union Data {
String s;
ShortString ss;
...
};
Исследуем код Rapidjson
l Кастомные аллокаторы;
l Микрооптимизации Neon/sse;
l header only – уровень оптимизации
определяется основным кодом.
Сравнительный тест
производительности (-O3).
l Nativejson benchmark (для некоторого i5 3
поколения)
Benchmarking Performance of C++ REST SDK (C++11)
Parse canada.json ... 162.880 ms 13.180 MB/s
Parse citm_catalog.json ... 23.060 ms 71.431 MB/s
Parse twitter.json ... 10.985 ms 54.826 MB/s
Benchmarking Performance of RapidJSON_FullPrec (C++)
Parse canada.json ... 15.184 ms 141.384 MB/s
Parse citm_catalog.json ... 3.109 ms 529.813 MB/s
Parse twitter.json ... 2.084 ms 288.992 MB/s
Сравнительный тест
производительности (-O0).
l Nativejson benchmark (для некоторого i5 3
поколения)
Benchmarking Performance of C++ REST SDK (C++11)
Parse canada.json ... 166.016 ms 12.931 MB/s
Parse citm_catalog.json ... 23.549 ms 69.947 MB/s
Parse twitter.json ... 11.044 ms 54.533 MB/s
Benchmarking Performance of RapidJSON_FullPrec (C++)
Parse canada.json ... 139.193 ms 15.423 MB/s
Parse citm_catalog.json ... 25.075 ms 65.691 MB/s
Parse twitter.json ... 10.639 ms 56.609 MB/s
Шаблоны выражений
l Быстрая, но громозкая реализация;
l Оборачиваем шаблоном, с более
короткой реализацией;
l Требуется оптимизация кода;
l Код по скорости близок к
идеальному.
template<typename T>
struct SArray {
explicit SArray(std::size_t s) : storage(new T[s]), storage_size(s) { ... }
SArray(SArray<T> const& orig) { … }
~SArray() { delete [] storage; }
std::size_t size() const { return storage_size; }
T const& operator[] (std::size_t idx) const {
return storage[idx];
}
T& operator[] (std::size_t idx) {
return storage[idx];
}
T* storage;
std::size_t storage_size;
};
Низкоуровневая
реализация
int main()
{
SArray<double> x(1000), y(1000);
for (auto i = 0 ; i < x.size(); ++i)
x[i] = 1.2 * x[i] + x[i] * y[i];
}
main:
push r12
mov edi, 8000
push rbp
sub rsp, 8
call operator new[](unsigned
long)@PLT
lea rdi, 8[rax]
...
call operator new[](unsigned
long)@PLT
lea rdi, 8[rax]
...
rep stosq
Готовый код конструкторы
.L2:
movupd xmm3, XMMWORD PTR 0[rbp+rax]
movupd xmm0, XMMWORD PTR [r8+rax]
mulpd xmm0, xmm3
movapd xmm1, xmm3
mulpd xmm1, xmm2
addpd xmm0, xmm1
movups XMMWORD PTR 0[rbp+rax], xmm0
add rax, 16
cmp rax, 8000
jne .L2
mov rdi, r8
call operator delete[](void*)@PLT
mov rdi, rbp
call operator delete[](void*)@PLT
x[i] = 1.2 * x[i] + x[i] * y[i];
Готовый код выражение.
template<typename T, typename OP1, typename OP2>
struct A_Mult {
typename A_Traits<OP1>::ExprRef op1; // first operand
typename A_Traits<OP2>::ExprRef op2; // second operand
A_Mult (OP1 const& a, OP2 const& b)
: op1(a), op2(b) { }
T operator[] (std::size_t idx) const {
return op1[idx] * op2[idx];
}
std::size_t size() const {
return std::max(a.size(), b.size());
}
};
Шаблон выражения
Скалярный тип.
template<typename T>
struct A_Scalar {
T const& scalar;
constexpr A_Scalar (T const& v) : scalar(v) { }
constexpr T const& operator[] (std::size_t) const {
return scalar;
}
constexpr std::size_t size() const { return 0; };
};
Обертка.
template<typename T, typename Rep = SArray<T>>
struct Array {
Rep expr_rep; // данные или класс-выражение
explicit Array (std::size_t s) : expr_rep(s) { }
Array (Rep const& rb) : expr_rep(rb) { }
Array& operator= (Array const& b) {
for (std::size_t idx = 0; idx<b.size(); ++idx) {
expr_rep[idx] = b[idx];
}
return *this;
}
std::size_t size() const { return expr_rep.size(); }
decltype(auto) operator[] (std::size_t idx) const {
return expr_rep[idx];
}
T& operator[] (std::size_t idx) {
return expr_rep[idx];
}
};
Обертка.
Оператор
template<typename T, typename R1, typename R2>
Array<T, A_Mult<T,R1,R2>>
operator* (Array<T,R1> const& a, Array<T,R2> const& b) {
return Array<T,A_Mult<T,R1,R2>>
(A_Mult<T,R1,R2>(a.rep(), b.rep()));
}
Как используется
int main()
{
Array<double> x(1000), y(1000);
x = 1.2 * x + x * y;
}
Выражение
Array<double, SArray<double> >&
Array<double, SArray<double> >::operator=<double,
A_Add<double, A_Mult<double, A_Scalar<double>,
SArray<double> >,
A_Mult<double, SArray<double>, SArray<double> > > >
(
Array<double,
A_Add<double, A_Mult<double, A_Scalar<double>,
SArray<double> >,
A_Mult<double, SArray<double>, SArray<double> > > >
const&
)
x = 1.2 * x + x * y;
main:
...
call operator new[](unsigned long)@PLT
...
call operator new[](unsigned long)@PLT
...
rep stosq
...
movaps XMMWORD PTR 80[rsp], xmm0
call Array<double, ...
mov rdi, QWORD PTR 32[rsp]
test rdi, rdi
je .L46
call operator delete[](void*)@PLT
.L46:
mov rdi, QWORD PTR 16[rsp]
test rdi, rdi
je .L47
call operator delete[](void*)@PLT
Array<double, SArray<double> >& Array<double, SArray<double>
>::operator=<double, A_Add<double, A_Mult<double, A_Scalar<double>,
SArray<double> >, A_Mult<double, SArray<double>, SArray<double> > >
>(Array<double, A_Add<double, A_Mult<double, A_Scalar<double>,
SArray<double> >, A_Mult<double, SArray<double>, SArray<double> > >
> const&):
push r15
...
.L26:
mov r9, QWORD PTR [rdi]
mov rsi, QWORD PTR [r12]
add rcx, 1
movsd xmm0, QWORD PTR [r9+rdx]
mov r9, QWORD PTR 0[rbp]
mulsd xmm0, QWORD PTR [rsi+rdx]
mov rsi, QWORD PTR [rbx]
movsd xmm1, QWORD PTR [r9+rdx]
mulsd xmm1, QWORD PTR [rsi]
addsd xmm0, xmm1
movsd QWORD PTR [r11], xmm0
benchmarks
static void Ideal(benchmark::State& state)
{
SArray<double> x(1000), y(1000);
for(auto _ : state)
{
for (auto i = 0 ; i < x.size(); ++i)
x[i] = 1.2 * x[i] + x[i] * y[i];
}
}
BENCHMARK(Ideal);
static void SuperFast(benchmark::State& state)
{
Array<double> x(1000), y(1000);
for(auto _ : state) {
x = 1.2 * x + x * y;
}
}
BENCHMARK(SuperFast);
Benchmark
gcc 9.2.0 (-O3)
Run on (8 X 3400 MHz CPU s)
CPU Caches:
L1 Data 32K (x4)
L1 Instruction 32K (x4)
L2 Unified 256K (x4)
L3 Unified 6144K (x1)
Load Average: 0.33, 0.35, 0.21
-----------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------
Ideal 255 ns 255 ns 2693296
SuperFast 346 ns 346 ns 2007497
Benchmark
gcc 9.2.0 (-O0)
Run on (8 X 3400 MHz CPU s)
CPU Caches:
L1 Data 32K (x4)
L1 Instruction 32K (x4)
L2 Unified 256K (x4)
L3 Unified 6144K (x1)
Load Average: 0.33, 0.35, 0.21
-----------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------
Ideal 10123 ns 10119 ns 63845
SuperFast 40989 ns 40971 ns 13674
Итог
l Rapidjson быстр, но требует оптимизацию
l Шаблоны выражений требуют
оптимизацию.
l Тестовая библиотека дает
- Rapidjson 15-25 МБ/с
- Cpp rest sdk 13 МБ/с
Исследуем проект
l Часть библиотек с /Z0 (== -O0)
l Часть с включенной оптимизацией
Архитектура кода и оптимизация :
модуль json.o
void readJ(std::string & json)
{
rapidjson::Document doc;
doc.Parse(json.c_str());
}
void readJExternal(std::string const& json, rapidjson::Document& doc)
{
doc.Parse(json.c_str());
}
Архитектура кода и оптимизация:
модуль main.o
static void JSON_ParseInternal(benchmark::State& state)
{
std::ifstream in_("canada.json");
auto json = std::string(std::istreambuf_iterator<char>(in_), std::istreambuf_iterator<char>());
for(auto _ : state) {
readJ(json);
}
}
BENCHMARK(JSON_ParseInternal);
Архитектура кода и оптимизация:
модуль main.o
static void JSON_ParseExternal(benchmark::State& state)
{
std::ifstream in_("canada.json");
auto json = std::string(std::istreambuf_iterator<char>(in_),
std::istreambuf_iterator<char>());
rapidjson::Document doc;
for(auto _ : state) {
readJExternal(json, doc);
}
}
BENCHMARK(JSON_ParseExternal);
Разделение кода и оптимизация:
модуль main.o
static void JSON_CalcSum(benchmark::State& state)
{
std::ifstream in_("canada.json");
auto json = std::string(std::istreambuf_iterator<char>(in_),
std::istreambuf_iterator<char>());
rapidjson::Document doc;
readJExternal(json, doc);
for(auto _ : state) {
const auto& a = doc["features"][0]["geometry"]["coordinates"][0];
double result = 0.0;
for(rapidjson::SizeType i = 0; i < a.Size(); ++i)
result += a[i][0].GetDouble() + a[i][1].GetDouble();
}
}
BENCHMARK(JSON_CalcSum);
Производтельность, разные
оптимизации, итераций за цикл
Main +
O0
Json +
O0
Main +
O3
Json O3
Main + O3
Json + O0
Main + O0
Json + O3
JSON_ParseInterna
l
7 78 7 78
JSON_ParseExterna
l
8 75 8 75
JSON_CalcSum
301592 4925519 4925519 301592
План
l Дотянуть оптимизацию в разных либах
l Перейти на rapidjson
l Можем добавить шаблоны выражений,
где требуется.
Кирилл
Березин
Ведущий программист
k.berezin@corp.mail.ru

Как не сделать врагами архитектуру и оптимизацию, Кирилл Березин, Mail.ru Group

  • 1.
    Кирилл Березин Ведущий программист Какне сделать врагами архитектуру и оптимизацию 14 Ноября 2019
  • 2.
    Постановка задачи ● Задача– сменить библиотеку json; ● Json использовался в качестве внутреннего формата данных; ● Использование шаблонов выражений для уменьшения кода.
  • 3.
    Зачем все этобыло? ● Исследование показало, что операции с json занимали не меньше 20 % времени; ● Rapidjson read rate 140 МБ/с (худший); ● Cpprest sdk 13 МБ/с.
  • 4.
    Архитектура программы l Функциональность вбиблиотеках; l Внутренний формат данных json-lib. dll Json wrapper dll exe
  • 5.
    Первые результаты l Результатперевода теста одной из библиотек показал: - Rapidjson 15-25 МБ/с - Cpp rest sdk 13 МБ/с
  • 6.
    Исследуем код cpprest typedefstd::vector<std::pair<utility::string_t, json::value>> storage_type;
  • 7.
    Исследуем код cpprest typedefstd::vector<std::pair<utility::string_t, json::value>> storage_type;
  • 8.
    Исследуем код cpprest lИспользуется move семантика; l Элементы могут быть сортированы – бинарный поиск; l Элементы полиморфные; l Поставляется в виде отдельной библиотеки.
  • 9.
    Исследуем код Rapidjson unionData { String s; ShortString ss; ... };
  • 10.
    Исследуем код Rapidjson lКастомные аллокаторы; l Микрооптимизации Neon/sse; l header only – уровень оптимизации определяется основным кодом.
  • 11.
    Сравнительный тест производительности (-O3). lNativejson benchmark (для некоторого i5 3 поколения) Benchmarking Performance of C++ REST SDK (C++11) Parse canada.json ... 162.880 ms 13.180 MB/s Parse citm_catalog.json ... 23.060 ms 71.431 MB/s Parse twitter.json ... 10.985 ms 54.826 MB/s Benchmarking Performance of RapidJSON_FullPrec (C++) Parse canada.json ... 15.184 ms 141.384 MB/s Parse citm_catalog.json ... 3.109 ms 529.813 MB/s Parse twitter.json ... 2.084 ms 288.992 MB/s
  • 12.
    Сравнительный тест производительности (-O0). lNativejson benchmark (для некоторого i5 3 поколения) Benchmarking Performance of C++ REST SDK (C++11) Parse canada.json ... 166.016 ms 12.931 MB/s Parse citm_catalog.json ... 23.549 ms 69.947 MB/s Parse twitter.json ... 11.044 ms 54.533 MB/s Benchmarking Performance of RapidJSON_FullPrec (C++) Parse canada.json ... 139.193 ms 15.423 MB/s Parse citm_catalog.json ... 25.075 ms 65.691 MB/s Parse twitter.json ... 10.639 ms 56.609 MB/s
  • 13.
    Шаблоны выражений l Быстрая,но громозкая реализация; l Оборачиваем шаблоном, с более короткой реализацией; l Требуется оптимизация кода; l Код по скорости близок к идеальному.
  • 14.
    template<typename T> struct SArray{ explicit SArray(std::size_t s) : storage(new T[s]), storage_size(s) { ... } SArray(SArray<T> const& orig) { … } ~SArray() { delete [] storage; } std::size_t size() const { return storage_size; } T const& operator[] (std::size_t idx) const { return storage[idx]; } T& operator[] (std::size_t idx) { return storage[idx]; } T* storage; std::size_t storage_size; };
  • 15.
    Низкоуровневая реализация int main() { SArray<double> x(1000),y(1000); for (auto i = 0 ; i < x.size(); ++i) x[i] = 1.2 * x[i] + x[i] * y[i]; }
  • 16.
    main: push r12 mov edi,8000 push rbp sub rsp, 8 call operator new[](unsigned long)@PLT lea rdi, 8[rax] ... call operator new[](unsigned long)@PLT lea rdi, 8[rax] ... rep stosq Готовый код конструкторы
  • 17.
    .L2: movupd xmm3, XMMWORDPTR 0[rbp+rax] movupd xmm0, XMMWORD PTR [r8+rax] mulpd xmm0, xmm3 movapd xmm1, xmm3 mulpd xmm1, xmm2 addpd xmm0, xmm1 movups XMMWORD PTR 0[rbp+rax], xmm0 add rax, 16 cmp rax, 8000 jne .L2 mov rdi, r8 call operator delete[](void*)@PLT mov rdi, rbp call operator delete[](void*)@PLT x[i] = 1.2 * x[i] + x[i] * y[i]; Готовый код выражение.
  • 18.
    template<typename T, typenameOP1, typename OP2> struct A_Mult { typename A_Traits<OP1>::ExprRef op1; // first operand typename A_Traits<OP2>::ExprRef op2; // second operand A_Mult (OP1 const& a, OP2 const& b) : op1(a), op2(b) { } T operator[] (std::size_t idx) const { return op1[idx] * op2[idx]; } std::size_t size() const { return std::max(a.size(), b.size()); } }; Шаблон выражения
  • 19.
    Скалярный тип. template<typename T> structA_Scalar { T const& scalar; constexpr A_Scalar (T const& v) : scalar(v) { } constexpr T const& operator[] (std::size_t) const { return scalar; } constexpr std::size_t size() const { return 0; }; };
  • 20.
    Обертка. template<typename T, typenameRep = SArray<T>> struct Array { Rep expr_rep; // данные или класс-выражение explicit Array (std::size_t s) : expr_rep(s) { } Array (Rep const& rb) : expr_rep(rb) { } Array& operator= (Array const& b) { for (std::size_t idx = 0; idx<b.size(); ++idx) { expr_rep[idx] = b[idx]; } return *this; }
  • 21.
    std::size_t size() const{ return expr_rep.size(); } decltype(auto) operator[] (std::size_t idx) const { return expr_rep[idx]; } T& operator[] (std::size_t idx) { return expr_rep[idx]; } }; Обертка.
  • 22.
    Оператор template<typename T, typenameR1, typename R2> Array<T, A_Mult<T,R1,R2>> operator* (Array<T,R1> const& a, Array<T,R2> const& b) { return Array<T,A_Mult<T,R1,R2>> (A_Mult<T,R1,R2>(a.rep(), b.rep())); }
  • 23.
    Как используется int main() { Array<double>x(1000), y(1000); x = 1.2 * x + x * y; }
  • 24.
    Выражение Array<double, SArray<double> >& Array<double,SArray<double> >::operator=<double, A_Add<double, A_Mult<double, A_Scalar<double>, SArray<double> >, A_Mult<double, SArray<double>, SArray<double> > > > ( Array<double, A_Add<double, A_Mult<double, A_Scalar<double>, SArray<double> >, A_Mult<double, SArray<double>, SArray<double> > > > const& ) x = 1.2 * x + x * y;
  • 25.
    main: ... call operator new[](unsignedlong)@PLT ... call operator new[](unsigned long)@PLT ... rep stosq ... movaps XMMWORD PTR 80[rsp], xmm0 call Array<double, ... mov rdi, QWORD PTR 32[rsp] test rdi, rdi je .L46 call operator delete[](void*)@PLT .L46: mov rdi, QWORD PTR 16[rsp] test rdi, rdi je .L47 call operator delete[](void*)@PLT
  • 26.
    Array<double, SArray<double> >&Array<double, SArray<double> >::operator=<double, A_Add<double, A_Mult<double, A_Scalar<double>, SArray<double> >, A_Mult<double, SArray<double>, SArray<double> > > >(Array<double, A_Add<double, A_Mult<double, A_Scalar<double>, SArray<double> >, A_Mult<double, SArray<double>, SArray<double> > > > const&): push r15 ... .L26: mov r9, QWORD PTR [rdi] mov rsi, QWORD PTR [r12] add rcx, 1 movsd xmm0, QWORD PTR [r9+rdx] mov r9, QWORD PTR 0[rbp] mulsd xmm0, QWORD PTR [rsi+rdx] mov rsi, QWORD PTR [rbx] movsd xmm1, QWORD PTR [r9+rdx] mulsd xmm1, QWORD PTR [rsi] addsd xmm0, xmm1 movsd QWORD PTR [r11], xmm0
  • 27.
    benchmarks static void Ideal(benchmark::State&state) { SArray<double> x(1000), y(1000); for(auto _ : state) { for (auto i = 0 ; i < x.size(); ++i) x[i] = 1.2 * x[i] + x[i] * y[i]; } } BENCHMARK(Ideal); static void SuperFast(benchmark::State& state) { Array<double> x(1000), y(1000); for(auto _ : state) { x = 1.2 * x + x * y; } } BENCHMARK(SuperFast);
  • 28.
    Benchmark gcc 9.2.0 (-O3) Runon (8 X 3400 MHz CPU s) CPU Caches: L1 Data 32K (x4) L1 Instruction 32K (x4) L2 Unified 256K (x4) L3 Unified 6144K (x1) Load Average: 0.33, 0.35, 0.21 ----------------------------------------------------- Benchmark Time CPU Iterations ----------------------------------------------------- Ideal 255 ns 255 ns 2693296 SuperFast 346 ns 346 ns 2007497
  • 29.
    Benchmark gcc 9.2.0 (-O0) Runon (8 X 3400 MHz CPU s) CPU Caches: L1 Data 32K (x4) L1 Instruction 32K (x4) L2 Unified 256K (x4) L3 Unified 6144K (x1) Load Average: 0.33, 0.35, 0.21 ----------------------------------------------------- Benchmark Time CPU Iterations ----------------------------------------------------- Ideal 10123 ns 10119 ns 63845 SuperFast 40989 ns 40971 ns 13674
  • 30.
    Итог l Rapidjson быстр,но требует оптимизацию l Шаблоны выражений требуют оптимизацию. l Тестовая библиотека дает - Rapidjson 15-25 МБ/с - Cpp rest sdk 13 МБ/с
  • 31.
    Исследуем проект l Частьбиблиотек с /Z0 (== -O0) l Часть с включенной оптимизацией
  • 32.
    Архитектура кода иоптимизация : модуль json.o void readJ(std::string & json) { rapidjson::Document doc; doc.Parse(json.c_str()); } void readJExternal(std::string const& json, rapidjson::Document& doc) { doc.Parse(json.c_str()); }
  • 33.
    Архитектура кода иоптимизация: модуль main.o static void JSON_ParseInternal(benchmark::State& state) { std::ifstream in_("canada.json"); auto json = std::string(std::istreambuf_iterator<char>(in_), std::istreambuf_iterator<char>()); for(auto _ : state) { readJ(json); } } BENCHMARK(JSON_ParseInternal);
  • 34.
    Архитектура кода иоптимизация: модуль main.o static void JSON_ParseExternal(benchmark::State& state) { std::ifstream in_("canada.json"); auto json = std::string(std::istreambuf_iterator<char>(in_), std::istreambuf_iterator<char>()); rapidjson::Document doc; for(auto _ : state) { readJExternal(json, doc); } } BENCHMARK(JSON_ParseExternal);
  • 35.
    Разделение кода иоптимизация: модуль main.o static void JSON_CalcSum(benchmark::State& state) { std::ifstream in_("canada.json"); auto json = std::string(std::istreambuf_iterator<char>(in_), std::istreambuf_iterator<char>()); rapidjson::Document doc; readJExternal(json, doc); for(auto _ : state) { const auto& a = doc["features"][0]["geometry"]["coordinates"][0]; double result = 0.0; for(rapidjson::SizeType i = 0; i < a.Size(); ++i) result += a[i][0].GetDouble() + a[i][1].GetDouble(); } } BENCHMARK(JSON_CalcSum);
  • 36.
    Производтельность, разные оптимизации, итерацийза цикл Main + O0 Json + O0 Main + O3 Json O3 Main + O3 Json + O0 Main + O0 Json + O3 JSON_ParseInterna l 7 78 7 78 JSON_ParseExterna l 8 75 8 75 JSON_CalcSum 301592 4925519 4925519 301592
  • 37.
    План l Дотянуть оптимизациюв разных либах l Перейти на rapidjson l Можем добавить шаблоны выражений, где требуется.
  • 38.