На мой вкус, если называешь свой велосипед именем стандартного контейнера, то он должен вести себя точно так же, как стандартный контейнер. Иначе, это будет мина замедленного действия, на которую с большой вероятностью, кто ни-буть наступит.
Например, если последовать вашему совету — убрать atomic rc из string, который рассматривается в статье, то код:
string a="abc";
string b=a;
process (a);
process (b);
Будет гарантированно рейсить в момент освобождения памяти, в которой лежит массив байтов "abc".
Что бы этого избежать, прийдется постоянно держать в голове, что в проекте своя реализация string, копии которого нельзя передавать в потоки.
Согласитесь, не плохая подстава для новых людей на проекте.
В свое время искал реализацию std::vector, с SSO оптимизацией (по аналогии с std::string, хранящий N элементов внутри себя, без дополнительных аллоков на куче)
Жаль, что string_view ничего не знает про время жизни объекта, на который ссылается. С одной стороны, это мощный и удобный инструмент, для тех кто понимает, что делает.
А с другой стороны string_view приносит еще сто-пятьсот способов выстрелить себе в ногу, особенно для новичков.
К сожалению, с учетом ограничения у меня на ноутбуке в контейнере — 8GB RAM, десятки гигобайт данных не переварить, тем более с полнотекстовым поиском.
Да датасет генерируется тут.
Однако, на этот датасете потребление памяти не очень показательно:
C включенным полнотекстовым индексом RSS — порядка 1 GB. С выключенным полнотекстом RSS ~ 300 MB.
Замерил цифры на датасете из 5М записей (~2.5гб исходных данных) с отключенным полнотекстом:
RSS всего процесса
3.9GB с отключенным кэшом десериализованных объектов на стороне Golang,
5.2GB с включенным кэшем в golang и прогретым.
Редис и тарантул к сожалению на датасете из 5М записей не смогли взлететь :( Возможно что-то делаю не так
2018/01/29 16:43:53 Seeding data to Redis
panic: MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
goroutine 1 [running]:
_/build/repo.(*RedisRepo).Seed(0xc42000e098, 0x4c4b40, 0xddfb3b)
/build/repo/redis.go:37 +0x4cc
_/build/repo.Start(0xc420016801, 0x7ffeca7ad94a, 0x5, 0x4c4b40)
/build/repo/repo.go:43 +0x171
main.main()
/build/main.go:19 +0x10e
2018/01/29 16:41:10 Seeding data to Tarantool
panic: Failed to allocate 546 bytes in slab allocator for memtx_tuple (0x2)
goroutine 1 [running]:
_/build/repo.(*TarantoolRepo).Seed(0xc42000e0a0, 0x4c4b40, 0xde360d)
/build/repo/tarantool.go:28 +0x414
_/build/repo.Start(0xc420016801, 0x7ffe2f1a0946, 0x9, 0x4c4b40)
/build/repo/repo.go:43 +0x171
main.main()
/build/main.go:19 +0x10e
Кажется, это тупиковый спор. Технически обе конструкции инициализируют массив значениями 0.
Лично для меня важнее заметить факт непредвиденной инициализации большого массива 0, для вас, что бы автор кода понимал, детали подкапотного действа в компиляторе.
Ради интереса погрепал исходники хромиума, который разбирается в статье. Авторы в 10 раз чаще используют подход arr[]={0}, чем arr[]={}
С одной стороны вопрос, конечно, субъективный.
Но с другой стороны, предпочитаю, что бы код максимально сам себя описывал.
То, что конструкция '={0}' протрет массив нулями, на мой взгляд, интуитивно понятнее, чем '={}'.
Согласен, прямо сейчас ни одна реализация не вычисляет, но стандарт c++17 допускает Temporary materialization prvalue в sizeof.
Да, *ptr это конечно не prvalue, но тем не менее — в какой то момент код очень похожий на sizeof *ptr может сломаться от nullptr в ptr.
Вот не согласен, что это системное решение.
До компилятора такой запрет не донесешь. А ревьювер — человек, может пропустить.
не sizeof(тип) а sizeof(переменная)
согласен, если уж дошло до memset, то это безопаснее, но опять же с кучей оговорок
sizeof(*переменная)
это плохо, потенциально можно словить nullptr dereference (что, конечно, маловероятно на практике, но тем не менее без проверки переменная != nullptr — эта конструкция является UB)
Поддерживаю политику отказа от memset в плюсовом коде — это действительно тот еще рассадник ошибок.
Однако, лучше инициализировать массив конструкцией вида int a[10000] = {0};.
При ревью кода конструкцию вида int a[10000] = {}; можно не заметить, а если в контексте задачи инициализация массива нулями не требуется, то это незаметная конструкция сильно ударит по производительности кода...
Странно — очень маленькие цифры у вас получились.
Мы запускали детектор лиц на базе OpenCV на hi3516 (ARM v7, 600MHZ), hard float point. Получили примерно 2-5 fps.
Думаю, на RPi, можно достичь похожих или даже лучших цифр без внешнего железа.
Я провозил год назад партию из 30 плат, произведенных аналогичным образом. Проблем на таможне не возникло.
По субьективным ощущениям, проблемы с таможней могут начаться от 50 штук
При выполнении запроса строится план исполнения, с учетом селективности и свойств индексов, селективности выборок и условий выборок в запросе.
Развернутый ответ как работает исполнитель запроса — скорее предмет большой статьи, приведу несколько примеров оптимизации:
SELECT * FROM table WHERE A = 2010 AND B = 300 AND C > 100 AND C < 200
Если есть композитный(составной индекс) по A+B, то он будет автоматически подставлен вместо двух отдельных выборок в индексы A и B, и суммарная сложность выборки по A и B будет O(1)
Если есть TREE индекс по C, то индекс C будет использован как основной и сложность выборки по C будет 2*O(log(N)), вместо сканирования со сравнением поля C
SELECT * FROM table WHERE A IN (1,2,3,4,5,6,7,8,9,10,...) AND B=200
результат выборки A IN (1,2,3,4,5,6,7,8,9,10,...) с M-го запроса будет закэширован, и будет отдаваться из кэша, со сложностью O(1)
SELECT * FROM table WHERE A > 1 ORDER BY C ASC OFFSET 100000 LIMIT 10
Если есть TREE индекс по C, то при первом запросе будут пред рассчитаны, и закэшированы позиции позиции всех элементов таблицы отсортированные по полю 'C', и при следующих запросах ресурсоемкая сортировка уже не потребуется
Да, предусмотрен. Работа с данными внутри Reindexera использует COW подход.
Более того, в сишном когде, в подавляющем большинстве случаев операция Select отрабатывает вообще в "zero-alloc" режиме.
Select возвращает что то типа COW shared_ptr на записи, находящиеся прямо в хранилище. Алокация и копирование произойдет только в случае, если будут конкурирующие Select и Update.
Вообще наша исходная задача — данных до ~1GB, ~3-4М записей: уметь их быстро искать/фильтровать по сложным критериям. На мой взгляд, это достаточно типовая задача, для достаточно большого количества проектов.
Бесспорно, вручную совладать конечно можно, но вопрос на засыпку:
как вручную сделать выборку из 100к записей по N критериям или с полнотекстовым поиском? Выносить всю логику выполнения запроса к БД на Application Level — не очень радостная перспектива то.
Если говорить про реальные тесты с бОльшим количеством данных — вот живой пример, участвовали с Reindexer в mail.ru highload cup — там было порядка 10м записей (если мне не изменяет память, общим объемом около 1GB и ограничение 4GB ОЗУ). В финале попал только Reindexer, остальные решения не прошли в финал — кто по скорости, кто в память не влез…
Но впрочем ради интереса, чуть позже как дойдут руки прогоню тесты из статьи на большем объеме данных — скажем 10-20м записей.
На мой вкус, если называешь свой велосипед именем стандартного контейнера, то он должен вести себя точно так же, как стандартный контейнер. Иначе, это будет мина замедленного действия, на которую с большой вероятностью, кто ни-буть наступит.
Например, если последовать вашему совету — убрать atomic rc из string, который рассматривается в статье, то код:
Будет гарантированно рейсить в момент освобождения памяти, в которой лежит массив байтов "abc".
Что бы этого избежать, прийдется постоянно держать в голове, что в проекте своя реализация string, копии которого нельзя передавать в потоки.
Согласитесь, не плохая подстава для новых людей на проекте.
Спасибо, отличная статья.
В свое время искал реализацию std::vector, с SSO оптимизацией (по аналогии с std::string, хранящий N элементов внутри себя, без дополнительных аллоков на куче)
Не нашел, в итоге написали свой.
Жаль, что string_view ничего не знает про время жизни объекта, на который ссылается. С одной стороны, это мощный и удобный инструмент, для тех кто понимает, что делает.
А с другой стороны string_view приносит еще сто-пятьсот способов выстрелить себе в ногу, особенно для новичков.
А как реализовать деструктор для COW строк без atomic ref counter?
(при условии, что наша реализации должна соответствовать стандарту C++, которая не запрещает передавать копии строки в другие потоки)
К сожалению, с учетом ограничения у меня на ноутбуке в контейнере — 8GB RAM, десятки гигобайт данных не переварить, тем более с полнотекстовым поиском.
5М записей (исходных данных 2.5GB) полнотекст отключен:
reindex byid -> 161491.19
reindex 1cond -> 66582.29
reindex 2cond -> 55052.78
reindex update -> 20879.24
1М записей
reindex byid -> 168354.18
reindex 1cond -> 65383.95
reindex 2cond -> 51218.16
reindex update -> 22164.61
Тарантул и Redis такой датасет в том же окружении не переварили
Да датасет генерируется тут.
Однако, на этот датасете потребление памяти не очень показательно:
C включенным полнотекстовым индексом RSS — порядка 1 GB. С выключенным полнотекстом RSS ~ 300 MB.
Замерил цифры на датасете из 5М записей (~2.5гб исходных данных) с отключенным полнотекстом:
RSS всего процесса
Редис и тарантул к сожалению на датасете из 5М записей не смогли взлететь :( Возможно что-то делаю не так
Кажется, это тупиковый спор. Технически обе конструкции инициализируют массив значениями 0.
Лично для меня важнее заметить факт непредвиденной инициализации большого массива 0, для вас, что бы автор кода понимал, детали подкапотного действа в компиляторе.
Ради интереса погрепал исходники хромиума, который разбирается в статье. Авторы в 10 раз чаще используют подход
arr[]={0}
, чемarr[]={}
Не вводите людей в заблуждение. Рекомендую прочитать reference перед тем, как громко что-то утверждать.
Конструкция
a[10000] = {0}
гарантированно инициализирует нулями весь массив на c++.С одной стороны вопрос, конечно, субъективный.
Но с другой стороны, предпочитаю, что бы код максимально сам себя описывал.
То, что конструкция '={0}' протрет массив нулями, на мой взгляд, интуитивно понятнее, чем '={}'.
Согласен, прямо сейчас ни одна реализация не вычисляет, но стандарт c++17 допускает Temporary materialization prvalue в sizeof.
Да, *ptr это конечно не prvalue, но тем не менее — в какой то момент код очень похожий на sizeof *ptr может сломаться от nullptr в ptr.
Вот не согласен, что это системное решение.
До компилятора такой запрет не донесешь. А ревьювер — человек, может пропустить.
согласен, если уж дошло до memset, то это безопаснее, но опять же с кучей оговорок
это плохо, потенциально можно словить nullptr dereference (что, конечно, маловероятно на практике, но тем не менее без проверки переменная != nullptr — эта конструкция является UB)
Поддерживаю политику отказа от memset в плюсовом коде — это действительно тот еще рассадник ошибок.
Однако, лучше инициализировать массив конструкцией вида
int a[10000] = {0};
.При ревью кода конструкцию вида
int a[10000] = {};
можно не заметить, а если в контексте задачи инициализация массива нулями не требуется, то это незаметная конструкция сильно ударит по производительности кода...Странно — очень маленькие цифры у вас получились.
Мы запускали детектор лиц на базе OpenCV на hi3516 (ARM v7, 600MHZ), hard float point. Получили примерно 2-5 fps.
Думаю, на RPi, можно достичь похожих или даже лучших цифр без внешнего железа.
Где возможно — стоимостный, где стоимость посчитать сложно — эвристический. Кэши — немного модифицированный LRU
А каким перевозчиком везли?
Я провозил год назад партию из 30 плат, произведенных аналогичным образом. Проблем на таможне не возникло.
По субьективным ощущениям, проблемы с таможней могут начаться от 50 штук
Конечно есть.
При выполнении запроса строится план исполнения, с учетом селективности и свойств индексов, селективности выборок и условий выборок в запросе.
Развернутый ответ как работает исполнитель запроса — скорее предмет большой статьи, приведу несколько примеров оптимизации:
SELECT * FROM table WHERE A = 2010 AND B = 300 AND C > 100 AND C < 200
Если есть композитный(составной индекс) по A+B, то он будет автоматически подставлен вместо двух отдельных выборок в индексы A и B, и суммарная сложность выборки по A и B будет O(1)
SELECT * FROM table WHERE A IN (1,2,3,4,5,6,7,8,9,10,...) AND B=200
SELECT * FROM table WHERE A > 1 ORDER BY C ASC OFFSET 100000 LIMIT 10
Да, предусмотрен. Работа с данными внутри Reindexera использует COW подход.
Более того, в сишном когде, в подавляющем большинстве случаев операция Select отрабатывает вообще в "zero-alloc" режиме.
Select возвращает что то типа COW shared_ptr на записи, находящиеся прямо в хранилище. Алокация и копирование произойдет только в случае, если будут конкурирующие Select и Update.
Вообще наша исходная задача — данных до ~1GB, ~3-4М записей: уметь их быстро искать/фильтровать по сложным критериям. На мой взгляд, это достаточно типовая задача, для достаточно большого количества проектов.
Бесспорно, вручную совладать конечно можно, но вопрос на засыпку:
Если говорить про реальные тесты с бОльшим количеством данных — вот живой пример, участвовали с Reindexer в mail.ru highload cup — там было порядка 10м записей (если мне не изменяет память, общим объемом около 1GB и ограничение 4GB ОЗУ). В финале попал только Reindexer, остальные решения не прошли в финал — кто по скорости, кто в память не влез…
Но впрочем ради интереса, чуть позже как дойдут руки прогоню тесты из статьи на большем объеме данных — скажем 10-20м записей.
Пока http API в статусе драфта, и будет немного меняться. Как финализируется сделаем подробную документацию.
По просьбе в комментарии выше, выложил на docker hub образ, который можно запустить, и в браузере подергать методы API через Swagger UI.
Выглядит вот так: