[Программирование, Dart] Ускоряем Dart. Нативно, недорого
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Релиз Dart 2.12 принёс, помимо всего прочего, поддержку FFI в стабильной версии, что позволит относительно легко добавить биндинги к своим любимым библиотекам, которые используют сишный ABI для экспорта. А это позволяют сделать в том числе и Rust, Go, Swift и другие.Считаем логарифмыДля начала попробуем написать синтетический код на чистом Dart, который будет выполняться относительно долго, но не слишком. Например, посчитаем сто миллионов логарифмов.Забегая вперёд, скажу что такой подход выбран, чтобы скомпилировать эквивалентный код на C так, чтобы компилятор его не вырезал в попытках оптимизации.
import 'dart:math';
// считаем n логарифмов функцией из стандартной библиотеки
double log_loop(int n) {
double l = 0;
for (int i = 0; i < n; i++) {
l = log(i);
}
return l;
}
Чтобы проверить результат, напишем минимальную функцию main, которая замеряет время выполнения
var w = Stopwatch()..start();
double result = log_loop(100000000);
print('${w.elapsed.inMilliseconds}');
Запустив полученный код без компиляции, используя dart run, узнаем что расчёты выполнялись 10.05 секунд.Конфигурацию системы, на которой происходило тестирование, сознательно нe привожу. Цели провести качественный бенчмарк в абсолютных значениях не было, и далее будут иметь значение только относительные показатели.Можно ли лучше? В принципе да. Выполняем dart compile exe и получаем ускорение на 10% или 9.00 секунд.Идём за горизонт событийЕсли для вас 10 секунд - это слишком долго, можно попробовать пострелять себе в ноги.Пишем эквивалентный код на C, используя log из <math.h>. Здесь и далее подразумевается, что log входит в состав libm из glibc, а код компилируется GCC 9.1.
// log_loop.c
#include <math.h>
double log_loop(int n) {
double l = 0;
for (int i = 0; i < n; i++) {
l = log(i);
}
return l;
}
Видите разницу? И я не вижу. Поэтому тоже сделаем небольшую обёртку чтобы замерить время, за которое посчитает реализация на C. Предполагаем, что код log_loop.c просто входит в состав main.c и функция main содержит следующий код:
clock_t ts = clock();
double result = log_loop(100000000);
printf("%.2f\n", 1000.0*(clock() - ts)/CLOCKS_PER_SEC);
Компилируя в обычный ELF при помощи gcc -o main -lm main.c увидим, что полученный бинарник выполняет свои действия за 1.69 секунд.Не мудрствуя лукаво, сразу пробуем скомпилировать с уровнями оптимизации O3, O2 и O1:
- 03 - 0.822 с
- 02 - 0.832 с
- 01 - 0.834 с
Как видно, разница не слишком большая, поэтому далее ограничимся рассмотрением только уровней O3 и O0.СтыкуемсяПеред тем, как использовать dart:ffi попробуем проверить, несёт ли какие-то накладные расходы подключение библиотек без использования FFI, непосредственно из кода на C, для которого сишные библиотеки совсем не инородные.В наш main.c добавим объявление double log_loop(int) вместо реализации, описанной выше.Предварительно компилируем log_loop.c в объектный файл через gcc -c -o log_loop.o log_loop.c и далее:
- ar rcs liblog_loop.a log_loop.o — для компиляции в статически линкуемый архив
- gcc -L. -o main -lm -llog_loop main.c — для сборки бинарника
В результате получим 1.69 секунд для O0 и 0.83 секунд для O3.Для разделяемых библиотек процесс примерно такой же. Отмечу, что эти же библиотеки и будут использоваться для подключения через dart:ffi:
- gcc -shared -o liblog_loop.so log_loop.o — для получения динамической библиотеки
- gcc -L. -o main -lm -llog_loop main.c — для сборки бинарника
Для запуска приложению будет нужна log_loop.so поэтому будем использовать LD_LIBRARY_PATH=..
Здесь результаты примерно такие же: 1.70 секунд для O0 и 0.83 секунд для O3.Начинаем рисковатьЧтобы использовать библиотеку через FFI, её надо подключить в рантайме. Используем для этого dlopen.В код библиотеки никаких изменений не вносим, а вот процесс подключения библиотеки в исполняемом файле существенно меняется:
#include <dlfcn.h>
// предполагаем, что ошибок не будет, поэтому для простоты пропускаем всю обработку
void *loader;
if ((loader = dlopen("liblog_loop.so", RTLD_NOW)) == NULL) // в конце нужно не забыть про dlclose(loader)
return 1;
double (*log_loop)(int);
*(void **)(&log_loop) = dlsym(l, "log_loop");
// аналогичным образом замеряем производительность
И в этом случае показатели абсолютно не меняются: 1.69 секунд для O0 и 0.83 секунд для O3.Небольшой итогНезависимо от того, каким образом вычисляющий метод попадает в бинарник - будь то dlopen, статическая или динамическая библиотека, непосредственно подключение библиотеки на время выполнения значительным образом не влияет, поэтому за базовую величину для дальнейшего сравнения примем, соответственно, 1.69 секунд для O0 и 0.83 секунд для O3.ПоехалиНу штош, приступим к тому, для чего так долго готовились.Готовим неправильноКак мы помним, 10 секунд нас не устраивают и мы хотим быстрее. Раз функция log есть в libm, то попробуем оттуда её и взять.Функцию main в нашем dart-коде оставим неизменной, а вот вычислительный метод будет таким:
import 'dart:ffi';
typedef LogFFI = Double Function(Double);
typedef Log = double Function(double);
double log_loop(int n) {
final libm = DynamicLibrary.open("/lib/.../libm.so"); // путь зависит от libc
double l = 0;
for (int i = 0; i < n; i++) {
final log = libm.lookup<NativeFunction<LogFFI>>('log').asFunction<Log>();
l = log(i * 1.0);
}
return l;
}
Принцип подключения разделяемых библиотек в Dart похож на работу с dlopen, но более типобезопасен. Здесь мы описываем сигнатуру для нативной функции double log(double)из libm.so через типы dart:ffi и приводим её к функции со встроенными типами Dart.Получаем, что теперь наш код выполняется за 70 секунд в скомпилированном виде и 75 секунд через dart run, что в 7 раз медленнее чем реализация на основе dart:math.Поиск и преобразование функции делать во время выполнения критичных операций не стоит. В синтетическом примере это довольно очевидная ошибка, но по невнимательности её допустить довольно просто.Перепишем функцию по-нормальному, вынеся lookup за пределы цикла:
double log_loop(int n) {
final libm = DynamicLibrary.open("/lib/.../libm.so"); // путь зависит от libc
final log = libm.lookup<NativeFunction<LogFFI>>('log').asFunction<Log>();
double l = 0;
for (int i = 0; i < n; i++) {
l = log(i * 1.0);
}
return l;
}
После таких манипуляций получим 2.96 секунд в скомпилированном виде и 3.08 секунд через dart run. Это в 3 раза быстрее оригинальной реализации, но всё же пока что примерно в 3.5 раза медленнее, чем самая быстрая нативная реализация на C.Делаем хорошоРаз мы уже имеем скомпилированные библиотеки, почему бы не взять готовое? Справедливое заключение, поэтому так и сделаем, подключив вместо libm готовую реализацию log_loop на C. Сначала определим типы для функции:
import 'dart:ffi';
// В Dart нет типа Int для нативных функций, поэтому пользуемся тем что есть
typedef LogLoopFFI = Double Function(IntPtr);
typedef LogLoop = double Function(int);
Код вычисления времени выполнении остаётся аналогичным самому первому примеру, за исключением предварительной работы по подключению библиотеки. Для этого в начале функции main просто добавим следующее (предполагая, что библиотеку мы расположили в директории lib/):
final loader = DynamicLibrary.open("lib/liblog_loop.so");
final log_loop = loader.lookup<NativeFunction<LogLoopFFI>>('log_loop').asFunction<LogLoop>();
Для библиотеки, скомпилированной с уровнем оптимизаций O3 получим 0.83 секунды для скомпилированного файла и 0.86 секунд для dart run.Как можно заметить, это ничем не отличается от нативной реализации, примерно в 10 раз быстрее, чем нативная реализация на Dart и в 2.5 раза эффективнее вызова logнапрямую из libm.Большой итогЗакругляясь, для наглядности приведу сводную табличку с результатами всех тестов:Среда-OexeВремя, секC0+1.6930.82С (shared)01.7030.83C (static)01.6930.83C (dlopen)01.6930.83Dart+9.00-10.1Dart (libm, loop lookup)+70.2-75.8Dart (libm, fixed)+2.96-3.07Dart (ffi)0+1.71-1.72Dart (ffi)3+0.83-0.86Как можно заметить, при правильном подходе среда выполнения практически не оказывает влияние на производительность при работе с FFI. При этом, несмотря на то, что вызов log через FFI ускоряет выполнение, хорошо видно влияние интерфейса между Dart и C.При сравнении с реализацией на C получим что на каждый вызов log Dart тратит дополнительно (2.96-0.83)×1e8/1e9 = 0.213 нс на каждую итерацию. Считая, что каждая операция выполняется за 0.82×1e8/1e9 = 0.082 нс получаем более чем двукратную разницу.К счастью, стоимость операции вызова функции постоянна (по крайней мере, для функций с одинаковыми аргументами), поэтому чем больше времени выполняется сторонний код, тем менее заметно будет влияние FFI. Но это ожидаемый исход и в целом такая картина характерна для реализации похожих интерфейсов в других языках.Стоит, однако, сделать ремарку, что работа со сложными типами вроде структур или передача больших объёмов данных через указатели, преобразование строк из Utf8Pointer и т. д. безусловно приведут к дополнительному оверхеду. В качестве бонуса отмечу существование пакета ffigen для генерации биндингов к библиотекам из сопутствующих заголовочных файлов.
===========
Источник:
habr.com
===========
Похожие новости:
- [Анализ и проектирование систем, Проектирование и рефакторинг, CAD/CAM, Инженерные системы] 10 главных усовершенствований в SOLIDWORKS Electrical 2021
- [Data Mining, Big Data, Data Engineering] Таксономия очистки данных форматов времени и дат (перевод)
- [Big Data, Хранение данных, Hadoop, Data Engineering] Пилотный Cloudera митап про новую платформу CDP пройдет 25.03 в 16:00
- [Разработка веб-сайтов, .NET, Компиляторы, C#, WebAssembly] Ahead-of-Time компиляция и Blazor
- [Программирование, C++, Программирование микроконтроллеров, Производство и разработка электроники] Создание аналога посмертного сore dump для микроконтроллера
- [Open source, Разработка мобильных приложений, Разработка под Android, GitHub, Kotlin] Легкий DataBinding для Android
- [Open source, *nix] FOSS News №61 – дайджест материалов о свободном и открытом ПО за 15-21 марта 2021 года
- [Программирование, Алгоритмы] Детская сказка программисту на ночь
- [Open source, Разработка под Linux, Софт] Фонд свободного программного обеспечения объявил победителей премии Free Software Awards 2020
- [Законодательство в IT, Патентование, IT-компании] Суд оштрафовал Apple на 300 миллионов долларов за нарушение патента
Теги для поиска: #_programmirovanie (Программирование), #_dart, #_dart, #_dartlang, #_ffi, #_cinterop, #_c, #_libraries, #_programmirovanie (
Программирование
), #_dart
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 24-Ноя 19:57
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Релиз Dart 2.12 принёс, помимо всего прочего, поддержку FFI в стабильной версии, что позволит относительно легко добавить биндинги к своим любимым библиотекам, которые используют сишный ABI для экспорта. А это позволяют сделать в том числе и Rust, Go, Swift и другие.Считаем логарифмыДля начала попробуем написать синтетический код на чистом Dart, который будет выполняться относительно долго, но не слишком. Например, посчитаем сто миллионов логарифмов.Забегая вперёд, скажу что такой подход выбран, чтобы скомпилировать эквивалентный код на C так, чтобы компилятор его не вырезал в попытках оптимизации. import 'dart:math';
// считаем n логарифмов функцией из стандартной библиотеки double log_loop(int n) { double l = 0; for (int i = 0; i < n; i++) { l = log(i); } return l; } var w = Stopwatch()..start();
double result = log_loop(100000000); print('${w.elapsed.inMilliseconds}'); // log_loop.c
#include <math.h> double log_loop(int n) { double l = 0; for (int i = 0; i < n; i++) { l = log(i); } return l; } clock_t ts = clock();
double result = log_loop(100000000); printf("%.2f\n", 1000.0*(clock() - ts)/CLOCKS_PER_SEC);
Здесь результаты примерно такие же: 1.70 секунд для O0 и 0.83 секунд для O3.Начинаем рисковатьЧтобы использовать библиотеку через FFI, её надо подключить в рантайме. Используем для этого dlopen.В код библиотеки никаких изменений не вносим, а вот процесс подключения библиотеки в исполняемом файле существенно меняется: #include <dlfcn.h>
// предполагаем, что ошибок не будет, поэтому для простоты пропускаем всю обработку void *loader; if ((loader = dlopen("liblog_loop.so", RTLD_NOW)) == NULL) // в конце нужно не забыть про dlclose(loader) return 1; double (*log_loop)(int); *(void **)(&log_loop) = dlsym(l, "log_loop"); // аналогичным образом замеряем производительность import 'dart:ffi';
typedef LogFFI = Double Function(Double); typedef Log = double Function(double); double log_loop(int n) { final libm = DynamicLibrary.open("/lib/.../libm.so"); // путь зависит от libc double l = 0; for (int i = 0; i < n; i++) { final log = libm.lookup<NativeFunction<LogFFI>>('log').asFunction<Log>(); l = log(i * 1.0); } return l; } double log_loop(int n) {
final libm = DynamicLibrary.open("/lib/.../libm.so"); // путь зависит от libc final log = libm.lookup<NativeFunction<LogFFI>>('log').asFunction<Log>(); double l = 0; for (int i = 0; i < n; i++) { l = log(i * 1.0); } return l; } import 'dart:ffi';
// В Dart нет типа Int для нативных функций, поэтому пользуемся тем что есть typedef LogLoopFFI = Double Function(IntPtr); typedef LogLoop = double Function(int); final loader = DynamicLibrary.open("lib/liblog_loop.so");
final log_loop = loader.lookup<NativeFunction<LogLoopFFI>>('log_loop').asFunction<LogLoop>(); =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_dart |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 24-Ноя 19:57
Часовой пояс: UTC + 5