#list
Несколько рандомных фактов.
1. Посмотрим на такой код:
Неожиданно, он не скомпилируется. Не скомпилируется он на моменте обращения к переменной
На самом деле переменные из structure binding не умеют захватываться в лямбды (это, стоит надеяться, временно). Немножко пояснений можно посмотреть тут.
Кстати примерно из-за этого же structure binding становится пацаном довольно неровным, потому что начинает ломать всякое NRVO. Будьте аккуратны.
2. Попался один интересный доклад на CppCon 2016 про оптимизацию вашего кода. Автор предлагал использовать принцип DRY не только в случае написания кода, но и с шаблонами:
Учитывая, что для каждого инстанцирования код класса по факту будет копироваться, стоит вынести всё что можно в базовый класс:
Так сказать взгляд с другой стороны.
Посмотрите доклад. Там прикона : )
3. При использовании
Руками полезность этой фичи я пока не ощутил, но говорят, что бывает удобно, когда хочешь показать пользователю, что некоторая функция не использует возвращаемое значение/в схемах с коллбеками и подобном.
И ещё немного ссылочек на выходные:
0. Интересный доклад о переписывании базы данных личных сообщений в Вк.
1. Обзорный доклад про очереди в highload.
2. Статья про то, как важно уметь отказываться от уже сделанного.
3. Удобное сокращение для стандартных стримов в C++: https://github.com/vitaut/_._ (sorry).
4. Статья про некоторые отличия в реализациях частей C++ от разных компиляторов.
5. Новый формат QOI (qoiformat.org), который неплохо сжимает картинки, на уровне PNG, но при этом в десятки раз быстрее, а его спецификация -- одна страница.
Несколько рандомных фактов.
1. Посмотрим на такой код:
struct Kek { int lol; int kek; };std::vector<Kek> keks;
for (const auto& [lol, kek] : keks) {
[=] { std::cout << lol << "\n"; }();
}Неожиданно, он не скомпилируется. Не скомпилируется он на моменте обращения к переменной
lol в лямбде. Странно, да?🤔На самом деле переменные из structure binding не умеют захватываться в лямбды (это, стоит надеяться, временно). Немножко пояснений можно посмотреть тут.
Кстати примерно из-за этого же structure binding становится пацаном довольно неровным, потому что начинает ломать всякое NRVO. Будьте аккуратны.
2. Попался один интересный доклад на CppCon 2016 про оптимизацию вашего кода. Автор предлагал использовать принцип DRY не только в случае написания кода, но и с шаблонами:
struct B {
virtual ~B() = default;
};
template <typename T>
struct D : B {
std::vector<int> get() const { return m_v; }
std::vector<int> m_v;
};Учитывая, что для каждого инстанцирования код класса по факту будет копироваться, стоит вынести всё что можно в базовый класс:
struct B {
virtual ~B() = default;
virtual std::vector<int> get() const { return m_v; }
std::vector<int> m_v;
};
template <typename T>
struct D : B {};Так сказать взгляд с другой стороны.
Посмотрите доклад. Там прикона : )
3. При использовании
std::function<void(args...)> (с возвращаемым типом void) к такому объекту можно кастовать любой функтор с такими же аргументами, но любым возвращаемым типом. То есть вот такое будет компилироваться:std::function<void()> f{[] -> int {return 1;}};Руками полезность этой фичи я пока не ощутил, но говорят, что бывает удобно, когда хочешь показать пользователю, что некоторая функция не использует возвращаемое значение/в схемах с коллбеками и подобном.
И ещё немного ссылочек на выходные:
0. Интересный доклад о переписывании базы данных личных сообщений в Вк.
1. Обзорный доклад про очереди в highload.
2. Статья про то, как важно уметь отказываться от уже сделанного.
3. Удобное сокращение для стандартных стримов в C++: https://github.com/vitaut/_._ (sorry).
4. Статья про некоторые отличия в реализациях частей C++ от разных компиляторов.
5. Новый формат QOI (qoiformat.org), который неплохо сжимает картинки, на уровне PNG, но при этом в десятки раз быстрее, а его спецификация -- одна страница.
👍3❤1
#common #java (сам офигел)
G1GC 1/2.
Я обещал рассказать про сборщики мусора в разных языках программирования, но как-то так вышло, что всё, что хочется вкинуть слишком много для одного раза, так что буду делиться частями, чтобы и вы не померли читать, и я не загнулся писать книжки вместо постов (даже Пиши сокращай не помогает).
Внутри Hotspot JVM (я так понял в мире жавы существует много разных vm прям как компиляторов в плюсах или интерпретаторов в питоне, но это каноничный от Sun microsystems и соответственно Oracle) есть несколько дефолтных сборщиков мусора. Сегодня по ним и пройдёмся.
SerialGC -- последовательный сборщик мусора, состоящий из DefNEW и Tenured. Первый занимается сборкой молодого поколения и является копирующим сборщиком мусора, второй же следит за взрослым поколением и является модифицированным алгоритмом mark-sweep-compact. В силу того, что на обоих этапах происходит перемещение, аллокация аналогично pooled-аллокаторам (выдали указатель, сдвинули cur в пуле).
ParallelGC (ParNEW) -- параллельный сборщик мусора, состоящий из параллельного копирующего для молодого поколения и параллельного mark-compact для взрослого поколения. Аллокация также линейная. Несмотря на параллельность, он всё ещё является stop-the-world, однако паузы в среднем меньше.
CMS -- параллельный сборщик мусора, пытающийся минимизировать паузы приложения и использовать фоновую сборку. При первой остановке приложения фиксируется граф объектов. Далее в фоновом режиме этот граф обходится. На следующей остановке учитываются изменения, которые могли произойти за время фонового обхода, после чего в фоне происходит sweep этап. Структурно используется параллельный копирующий сборщик для молодого поколения, а для взрослого поколения фоновый mark-sweep. Аллокация происходит из freelist'ов, что приводит к фрагментации. Компактификация происходит только в случае, когда памяти начинает не хватать, что приводит к полной сборке мусора (для CMS это полный stop-the-world в один поток🤢). В последних версиях джавы был удалён, т.к. авторы перестали поддерживать.
Я так понял, что первые два есть просто по приколу, и можно юзать их, если вам прям ну не критично совсем, что там да как у вас работает или отдохнуть прилегло. CMS рекомендовался, если хочется паузы поменьше, но куча приложения не оч большая (до 2Гб).
G1GC 1/2.
Я обещал рассказать про сборщики мусора в разных языках программирования, но как-то так вышло, что всё, что хочется вкинуть слишком много для одного раза, так что буду делиться частями, чтобы и вы не померли читать, и я не загнулся писать книжки вместо постов (даже Пиши сокращай не помогает).
Внутри Hotspot JVM (я так понял в мире жавы существует много разных vm прям как компиляторов в плюсах или интерпретаторов в питоне, но это каноничный от Sun microsystems и соответственно Oracle) есть несколько дефолтных сборщиков мусора. Сегодня по ним и пройдёмся.
SerialGC -- последовательный сборщик мусора, состоящий из DefNEW и Tenured. Первый занимается сборкой молодого поколения и является копирующим сборщиком мусора, второй же следит за взрослым поколением и является модифицированным алгоритмом mark-sweep-compact. В силу того, что на обоих этапах происходит перемещение, аллокация аналогично pooled-аллокаторам (выдали указатель, сдвинули cur в пуле).
ParallelGC (ParNEW) -- параллельный сборщик мусора, состоящий из параллельного копирующего для молодого поколения и параллельного mark-compact для взрослого поколения. Аллокация также линейная. Несмотря на параллельность, он всё ещё является stop-the-world, однако паузы в среднем меньше.
CMS -- параллельный сборщик мусора, пытающийся минимизировать паузы приложения и использовать фоновую сборку. При первой остановке приложения фиксируется граф объектов. Далее в фоновом режиме этот граф обходится. На следующей остановке учитываются изменения, которые могли произойти за время фонового обхода, после чего в фоне происходит sweep этап. Структурно используется параллельный копирующий сборщик для молодого поколения, а для взрослого поколения фоновый mark-sweep. Аллокация происходит из freelist'ов, что приводит к фрагментации. Компактификация происходит только в случае, когда памяти начинает не хватать, что приводит к полной сборке мусора (для CMS это полный stop-the-world в один поток🤢). В последних версиях джавы был удалён, т.к. авторы перестали поддерживать.
Я так понял, что первые два есть просто по приколу, и можно юзать их, если вам прям ну не критично совсем, что там да как у вас работает или отдохнуть прилегло. CMS рекомендовался, если хочется паузы поменьше, но куча приложения не оч большая (до 2Гб).
👍3❤1
#common #java
G1GC 2/2.
G1 (garbage-first/G one) является сборщиком мусора общего назначения (i.e. работает со всей кучей). Он является параллельным фоновым сборщиком мусора, который пытается минимизировать паузы приложения. Основной абстракцией является сборка поколениями: существует молодое и взрослое поколение. Молодое поколение содержит три пула: основной (eden) и два дополнительных (survivors). Основной пулл содержит самые молодые объекты, два дополнительных используются для копирующей сборки мусора внутри молодого поколения (то есть внутри молодого поколения существует разделение на подпоколения: прям ну совсем молодые чуваки и объекты, пережившие хотя бы одну сборку). Когда в молодом поколении недостаточно места для новой аллокации, происходит малая сборка мусора. Она заключается в том, что все выжившие в eden объекты перемещаются в один из пулов для копирующей сборки.
Элементы из копирующего сборщика могут быть либо удалены, либо перемещены внутри копирующего сборщика, либо перенесены во взрослое поколение (если сборщик мусора примет такое решение).
Вся куча приложения делится на регионы размером от 1Мб до 32Мб. Каждый регион динамически относится в молодое/взрослое поколение (после сборок мусора регионы могут возвращаться во множество свободных и получать новую роль по необходимости).
В случае молодого регион может выполнять роль eden или survivor.
Особенная роль отводится большим объектам, которые не могут поместиться в один регион. Для них выделяется несколько рядом лежащих регионов. В процессе сборки мусора для таких объектов применяется отдельная логика (правда не обнаружил, какая именно, но вроде как там чуваки борются за дефрагментацию).
Во время сборки мусора выбирается множество регионов, которые будут очищаться (collection set). В него входят все регионы из молодого поколения и возможно некоторые из взрослого поколения (тут есть разделение по типу collection set'а: сборка только в молодых регионах, смешанная сборка и FullGC). Недостижимые объекты удаляются, а достижимые перемещаются в свободные регионы, которые назначаются либо регионами для взрослого поколения, либо survivor-регионами. При хорошем выборе регионов для очистки и засчёт compact новые регионы занимают меньше места, чем занимали объекты ранее (но не обязательно).
Для того, чтобы понимать, какие регионы из взрослого поколения стоит брать в очередной collection set, у каждого региона имеется remembered set, хранящий взаимосвязи объектов между регионами. То есть если объект из региона A ссылается на объект из региона B, то в rset'е B будет запись A. На основе этого ребята делают какие-то выводы.
Все этапы это stop-the-world, но есть этап фоновой маркировки, который стартует при достижении некоторого порога заполненности всей памяти приложения. Тут происходит несколько действий: обновляется информация о достижимости по регионам, освобождение регионов без живых объектов, устранение циклических зависимостей между неживыми объектами.
Когда хорошо юзать:
+ хотите паузы <0.5-1s;
+ минимальные настройки;
+ размер кучи >5Гб;
+ вам норм чиститься только когда больше половины кучи занято;
+ скорость создания объектов сильно меняется;
+ боремся с фрагментацией.
По настройкам кстати флагов у него хватает (кстати они оч забавные:
Вообще вся инфа из доклада одного из разрабов Oracle. Дока.
В планах ещё один java gc и бегло по другиим языкам, потому что пока чего-то крутого/подробного я в них не нашёл.
G1GC 2/2.
G1 (garbage-first/G one) является сборщиком мусора общего назначения (i.e. работает со всей кучей). Он является параллельным фоновым сборщиком мусора, который пытается минимизировать паузы приложения. Основной абстракцией является сборка поколениями: существует молодое и взрослое поколение. Молодое поколение содержит три пула: основной (eden) и два дополнительных (survivors). Основной пулл содержит самые молодые объекты, два дополнительных используются для копирующей сборки мусора внутри молодого поколения (то есть внутри молодого поколения существует разделение на подпоколения: прям ну совсем молодые чуваки и объекты, пережившие хотя бы одну сборку). Когда в молодом поколении недостаточно места для новой аллокации, происходит малая сборка мусора. Она заключается в том, что все выжившие в eden объекты перемещаются в один из пулов для копирующей сборки.
Элементы из копирующего сборщика могут быть либо удалены, либо перемещены внутри копирующего сборщика, либо перенесены во взрослое поколение (если сборщик мусора примет такое решение).
Вся куча приложения делится на регионы размером от 1Мб до 32Мб. Каждый регион динамически относится в молодое/взрослое поколение (после сборок мусора регионы могут возвращаться во множество свободных и получать новую роль по необходимости).
В случае молодого регион может выполнять роль eden или survivor.
Особенная роль отводится большим объектам, которые не могут поместиться в один регион. Для них выделяется несколько рядом лежащих регионов. В процессе сборки мусора для таких объектов применяется отдельная логика (правда не обнаружил, какая именно, но вроде как там чуваки борются за дефрагментацию).
Во время сборки мусора выбирается множество регионов, которые будут очищаться (collection set). В него входят все регионы из молодого поколения и возможно некоторые из взрослого поколения (тут есть разделение по типу collection set'а: сборка только в молодых регионах, смешанная сборка и FullGC). Недостижимые объекты удаляются, а достижимые перемещаются в свободные регионы, которые назначаются либо регионами для взрослого поколения, либо survivor-регионами. При хорошем выборе регионов для очистки и засчёт compact новые регионы занимают меньше места, чем занимали объекты ранее (но не обязательно).
Для того, чтобы понимать, какие регионы из взрослого поколения стоит брать в очередной collection set, у каждого региона имеется remembered set, хранящий взаимосвязи объектов между регионами. То есть если объект из региона A ссылается на объект из региона B, то в rset'е B будет запись A. На основе этого ребята делают какие-то выводы.
Все этапы это stop-the-world, но есть этап фоновой маркировки, который стартует при достижении некоторого порога заполненности всей памяти приложения. Тут происходит несколько действий: обновляется информация о достижимости по регионам, освобождение регионов без живых объектов, устранение циклических зависимостей между неживыми объектами.
Когда хорошо юзать:
+ хотите паузы <0.5-1s;
+ минимальные настройки;
+ размер кучи >5Гб;
+ вам норм чиститься только когда больше половины кучи занято;
+ скорость создания объектов сильно меняется;
+ боремся с фрагментацией.
По настройкам кстати флагов у него хватает (кстати они оч забавные:
-XX:MaxGCPauseMillis). Можно не пытаться подбирать такие конфигурации размеров регионов, что время пауз станет нормальным, а указать само время, и gc сам попытается подстроиться (не обещает, но постарается).Вообще вся инфа из доклада одного из разрабов Oracle. Дока.
В планах ещё один java gc и бегло по другиим языкам, потому что пока чего-то крутого/подробного я в них не нашёл.
❤1
#list
Начал разбирать посты из блогов разных умных челов за последние несколько месяцев. Так что накидаю, что там обнаружилось.
1. Уже сто раз была инфа про фичи C++20, но я опять обнаружил для себя что-то новое.
Например:
- наличие std::source_location. Такая стильно-модно-молодёжная замена
- условный
- std::lerp -- линейная интерполяция.
- std::midpoint -- аккуратное вычисление среднего.
2. Про вот это уже писал, но тем не менее. Немножко больше кейсов, когда typename можно явно не писать.
Вот такой код по идее должен компилится. И в gcc 9 его поддержали, а вот clang жмотится и не хочет: marked-строки не дадут программе заработать.
---
Если у вас есть опыт перехода/наблюдения за ним на C++20 в проде, расскажите кратенько, как это происходило (с размером команды, которую эту задевает). Учитывая, как хорошо экосистема плюсов (компиляторы, cmake) готовы на данный момент к 20ому стандарте, есть ощущение, что надо бы какой-нибудь стандарт вроде 29ого сдвинуть на годик-два вперёд, чтобы у всего вокруг были шансы догнать. А то так и сидим без модулей в cmake и без компиляторов, которые умеют в стандарт двухлетней давности.
---
3. Думаю, на @experimentalchill подписано в окрестности 100% читателей этого канала, но если вдруг нет, вот недавняя приконая статья про исправление сортировки: оригинал и перевод.
4. Прикиньте, что в C есть: VLA и flexible array member.
Первое это делать что-то вроде:
а второе:
5. Тестик на знание C.
6. Узнал, что чуваки в go завезли оператор A AND (NOT B), который выглядит как &^ , у которого ещё есть форма с равно: &^= . Они норм вообще))
Обычный ксор это
Вот кстати какая-то статья про неординарность этого языка.
7. Почему оно не находится. Андрей Аксёнов какой-то забавный чел. Прикольно рассказывает. В данном случае про то, как с нуля строить поиск.
Начал разбирать посты из блогов разных умных челов за последние несколько месяцев. Так что накидаю, что там обнаружилось.
1. Уже сто раз была инфа про фичи C++20, но я опять обнаружил для себя что-то новое.
Например:
- наличие std::source_location. Такая стильно-модно-молодёжная замена
__FILE__, __LINE__ и __PRETTY_FUNCTION__.- условный
explicit i.e. explicit(bool). Статья с понятными кейсами.- std::lerp -- линейная интерполяция.
- std::midpoint -- аккуратное вычисление среднего.
2. Про вот это уже писал, но тем не менее. Немножко больше кейсов, когда typename можно явно не писать.
template <typename T>
struct A : T::x {
using type = T::type; // marked
int v{T::val};
T::type x; // marked
};Вот такой код по идее должен компилится. И в gcc 9 его поддержали, а вот clang жмотится и не хочет: marked-строки не дадут программе заработать.
---
Если у вас есть опыт перехода/наблюдения за ним на C++20 в проде, расскажите кратенько, как это происходило (с размером команды, которую эту задевает). Учитывая, как хорошо экосистема плюсов (компиляторы, cmake) готовы на данный момент к 20ому стандарте, есть ощущение, что надо бы какой-нибудь стандарт вроде 29ого сдвинуть на годик-два вперёд, чтобы у всего вокруг были шансы догнать. А то так и сидим без модулей в cmake и без компиляторов, которые умеют в стандарт двухлетней давности.
---
3. Думаю, на @experimentalchill подписано в окрестности 100% читателей этого канала, но если вдруг нет, вот недавняя приконая статья про исправление сортировки: оригинал и перевод.
4. Прикиньте, что в C есть: VLA и flexible array member.
Первое это делать что-то вроде:
void f(int n) {
int arr[n];
}а второе:
struct S {
int len;
double arr[];
};5. Тестик на знание C.
6. Узнал, что чуваки в go завезли оператор A AND (NOT B), который выглядит как &^ , у которого ещё есть форма с равно: &^= . Они норм вообще))
Обычный ксор это
a^b, а инвертирование тем же символом ^a. Накрутили, прям как в плюсах.. Вот кстати какая-то статья про неординарность этого языка.
7. Почему оно не находится. Андрей Аксёнов какой-то забавный чел. Прикольно рассказывает. В данном случае про то, как с нуля строить поиск.
👍4❤1
#common #java
Java GC 1/2.
Из популярного в java ещё есть сборщик мусора Shenandoah. Как и G1, шенонда -- region based concurrent сборщик мусора.
Сначала происходит параллельная маркировка с двумя паузами до/после. В результате для каждого занятого региона становится понятно, как много живых объектов в нём содержится. Из регионов с небольшим количеством достижимых объектов формируется collection set, из которого происходит фоновое уплотнение в новый регион (concurrent evacuation).
После уплотнения нельзя считать, что регионы, из которых происходило уплотнение, можно ощищать.
На данные в них могут существовать ссылки из других регионов, потому следующим этапом производится перезапись ссылок с данных из старых регионов на данные в уплотнённом (concurrent update refs; тоже паузы до/после). После этого можно считать регионы очищенными и аллоцировать из них память.
Java GC 1/2.
Из популярного в java ещё есть сборщик мусора Shenandoah. Как и G1, шенонда -- region based concurrent сборщик мусора.
Сначала происходит параллельная маркировка с двумя паузами до/после. В результате для каждого занятого региона становится понятно, как много живых объектов в нём содержится. Из регионов с небольшим количеством достижимых объектов формируется collection set, из которого происходит фоновое уплотнение в новый регион (concurrent evacuation).
После уплотнения нельзя считать, что регионы, из которых происходило уплотнение, можно ощищать.
На данные в них могут существовать ссылки из других регионов, потому следующим этапом производится перезапись ссылок с данных из старых регионов на данные в уплотнённом (concurrent update refs; тоже паузы до/после). После этого можно считать регионы очищенными и аллоцировать из них память.
❤1
#common #java
Java GC 2/2.
Ещё одним известным сборщиком мусора в Java является ZGC. По принципу действия он аналогичен Shenandoah с точностью до некоторых локальных оптимизаций, которые, тем не менее, иногда играют значительную роль в использовании. Важным достижением ZGC являются константные stop-the-world паузы.
А ещё есть no-op EpsilonGC.
Вообще весь движ начинается в момент реализации барьеров на чтение/запись. Там какие-то java-специфичные вещи довольно. Если интересно, можно посмотреть докладик Алексея Шипилёва (я так понял, это Полухин из мира жавы).
На картиночке условное сравнение, чтобы выбрать gc по нуждам.
[1]. Shenandoah 1.
[2]. Shenandoah 2.
P.S. На правах рекламы вкину статью про обучение очень больших моделек. Что делать, если они не помещаются на GPU, и как с этим вообще жить: link. Вдруг тут есть любители машинок : )
Java GC 2/2.
Ещё одним известным сборщиком мусора в Java является ZGC. По принципу действия он аналогичен Shenandoah с точностью до некоторых локальных оптимизаций, которые, тем не менее, иногда играют значительную роль в использовании. Важным достижением ZGC являются константные stop-the-world паузы.
А ещё есть no-op EpsilonGC.
Вообще весь движ начинается в момент реализации барьеров на чтение/запись. Там какие-то java-специфичные вещи довольно. Если интересно, можно посмотреть докладик Алексея Шипилёва (я так понял, это Полухин из мира жавы).
На картиночке условное сравнение, чтобы выбрать gc по нуждам.
[1]. Shenandoah 1.
[2]. Shenandoah 2.
P.S. На правах рекламы вкину статью про обучение очень больших моделек. Что делать, если они не помещаются на GPU, и как с этим вообще жить: link. Вдруг тут есть любители машинок : )
👍3❤1
#list
Очередная пачка рандомной инфы.
1. Пару дней дебагал уб. Половину этого времени потратил на то, чтобы исключить false positive срабатывания санитайзеров на fmtlib и хедер stl с итераторами. Делается это при использовании блеклиста санитайзеров. Не знал, что такое есть. Кстати не отдебагал.
2. Недавно стал свидетелем дискуссии про то, как парсить плюсы (навеянной вот этим докладом). Если кратко, предлагается упростить синтаксис плюсов до некоторого достаточного множества конструкций, чтобы не парсить сложные завороты больной фантазии программиста. Умные дяди сказали, что писать самому звучит как не успех, но можно взять популярные опенсорсные проекты вроде tree-sitter.
3. Есть такой факт, что лямбды с пустым списком захвата умеют каститься к указателю на функцию:
Можно явно указать шаблонный тип:
используя builtin
4. Оказывается в большинстве файлов стандартной библиотеки go можно найти флаг
5. C++ sucks.
6. Я человек простой: вижу Аксёнова -- ставлю лайк.
У него оказывается в целом много обзорных докладов про всякие сферы программирования. Вот тут он рассказывает про различные способы сжатия.
Мне оч понравился факт, что дельта-кодирование используется в жизни. Дефолтная задача звучит как что-то вроде есть 10^8 нулей и запросы добавить на отрезок [l; r] число x. После всех запросов вывести итоговый массив. Давайте в
В докладе приводится пример про то, как использовать похожий подход при сжатии неубывающей последовательности чисел. Давайте вместо самой последовательности (например)
построим такую (первое оставляем, остальные заменяем на дельту относительно прошлого):
Теперь все числа в абсолютных значения сильно меньше. Уникальных тоже стало меньше. Можно пробовать жать их более эффективно. Утверждается, что такой подход можно найти в любом более менее популярном поиске вроде гугла, яндеха, sphinx или lucene (про последние может расскажу чуть позже).
7. Крышесносный факт (для меня точно). Существуют не только enum-классы, а и enum-структуры:
Которые вообще ничем не отличаются.
Очередная пачка рандомной инфы.
1. Пару дней дебагал уб. Половину этого времени потратил на то, чтобы исключить false positive срабатывания санитайзеров на fmtlib и хедер stl с итераторами. Делается это при использовании блеклиста санитайзеров. Не знал, что такое есть. Кстати не отдебагал.
2. Недавно стал свидетелем дискуссии про то, как парсить плюсы (навеянной вот этим докладом). Если кратко, предлагается упростить синтаксис плюсов до некоторого достаточного множества конструкций, чтобы не парсить сложные завороты больной фантазии программиста. Умные дяди сказали, что писать самому звучит как не успех, но можно взять популярные опенсорсные проекты вроде tree-sitter.
3. Есть такой факт, что лямбды с пустым списком захвата умеют каститься к указателю на функцию:
typedef int (*fptr_t)();
fptr_t create() {
fptr_t fptr = [] { return 2; }
return fptr;
}
template <typename T>
T apply (T(*f)()) {
return f();
}
int main() {
fptr_t f = create();
apply(f); // компилируется
apply([] {return 3;}); // не компилируется
}Можно явно указать шаблонный тип:
apply<int>(...), но так мы убиваем автоматический вывод типов. А если там что-то более сложное? Можно сделать static_cast<fptr_t>. А можно сделать так:apply(+[] {return 3; });используя builtin
T* operator+(T*). Как по мне магия какая-то)4. Оказывается в большинстве файлов стандартной библиотеки go можно найти флаг
debugMode, который по дефолту стоит false. Если ручками поменять его значение на true, начнётся дебаг вывод всякой служебной инфы. Например можно узнать, что даже при запуске пустой программы у вас создаётся два канала (один на сборщик мусора, один на что-то ещё). Это из доклада про устройство каналов.5. C++ sucks.
6. Я человек простой: вижу Аксёнова -- ставлю лайк.
У него оказывается в целом много обзорных докладов про всякие сферы программирования. Вот тут он рассказывает про различные способы сжатия.
Мне оч понравился факт, что дельта-кодирование используется в жизни. Дефолтная задача звучит как что-то вроде есть 10^8 нулей и запросы добавить на отрезок [l; r] число x. После всех запросов вывести итоговый массив. Давайте в
arr[l] добавлять x, а из arr[r+1] отнимать его. Во время вывода ответа держим текущую сумму и обновляем её из массива.В докладе приводится пример про то, как использовать похожий подход при сжатии неубывающей последовательности чисел. Давайте вместо самой последовательности (например)
[12, 18, 19, 31, ..., 228227, 228228, 228229]построим такую (первое оставляем, остальные заменяем на дельту относительно прошлого):
[12, 6, 1, 12, ..., 1, 1, 1]Теперь все числа в абсолютных значения сильно меньше. Уникальных тоже стало меньше. Можно пробовать жать их более эффективно. Утверждается, что такой подход можно найти в любом более менее популярном поиске вроде гугла, яндеха, sphinx или lucene (про последние может расскажу чуть позже).
7. Крышесносный факт (для меня точно). Существуют не только enum-классы, а и enum-структуры:
enum struct A {asd};Которые вообще ничем не отличаются.
👍10❤1
#cpp #common и может #algo
Вроде как знание о векторах в плюсах довольно тривиальное, но тем не менее.
Общеизвестный факт, что вектор это по факту такая структурка:
Кажется, сделать динамический массив меньшими усилиями ну никак нельзя. В самописных версиях иногда вместо двух чиселок также хранят указатели, а size/capacity вычисляют каждый раз. Можно упороться и хранить их в одной
Стандартная библиотека предлагает для кастомизации своего вектора подсунуть свой аллокатор вторым шаблонным аргументом. И всё. Больше пользователю ничего не надо. Но ведь есть ещё такая важная характеристика как resize policy -- то, как ваш вектор изменяет свой размер. Как много выделить в первой аллокации? 1, 2, 8? С единицы начинать плохо: после первого push_back почти наверняка придёт второй. С восьми тоже плохо: может у нас вектор объектов, каждый из которых 3Мб, а юзать мы хотим всего для трёх элементов. Да и для двойки свой кейс можно придумать.
Теперь про изменение этого размера. В clang довольно дефолтная схема с умножением на 2 (с оговорками). Но есть ощущение, что умножение на 2 оч плохое решение. Точнее, умножение на любую константу плохо. Когда речь идёт о расширении 8->16, это вполне себе хорошее решение (или нет?🧐без знания размера объектов непонятно). Но когда у вас уже 1кк элементов и вы хотите положить ещё пару штук, расширение 1кк->2кк выглядит как из пушки по воробьям.
И что делать?
Как вариант не расти всегда с одинаковой скоростью. Можно смотреть на текущее кол-во элементов. Можно на занимаемую память. И после выбора какой-то границы, расти в 2 раза до неё и в 1.2 после (понятно что тут стоит покрутить и найти компромисс между занимаемой памятью и временем работы в зависимости от ваших данных и паттерна работы с ними). В go в этом плане примерно так и сделано.
Ещё есть вопрос, уменьшаться ли, если размер стал слишком маленький: если у меня capacity на 1кк, а размер 10, то наверное можно было бы пожаться до этих самых 10, а потом расти по обычным правилам. Вроде как так почти никто не делает. Правда слышал, что в каком-то языке реализовано сжатие, если размер в 4 раза меньше вместимости (сходу не вспомнил где).
В студвекторе из подобного есть метод shrink_to_fit, который уменьшает вместимость до размера. Прошу заметить, что метод этот non-binding, i.e. необязательный. Тут авторы вольны делать что хотят. В clang честно пытаются уменьшиться.
Учитывая, что resize policy это что-то, что нельзя описать одним числом, интересно попробовать придумать, как это могло бы выглядеть в стандартной библиотеке. Что-то такое:
где Recap может выглядеть как что-то вроде
Понятно, что всё это можно делать и сейчас, вычисляя новый размер/капасити снаружи и используя
Возвращаясь к теме пооптимизировать вектор, кроме всем известной специализации для булов иногда пишут специализацию для POD-типов, где можно не вызывать конструкторы/деструкторы при различных операциях.
Можно ещё писать контейнеры кусками (вроде дека), чтобы немножко получше жить с аллокациями. Или пробовать хранить мало данных на стеке, прям как sso.
Зачем это всё? А хер его знает. Почти наверняка на практике это вам не пригодится, потому что если вам не хватает
Такие дела.
Вроде как знание о векторах в плюсах довольно тривиальное, но тем не менее.
Общеизвестный факт, что вектор это по факту такая структурка:
template <typename T>
struct vector {
T* data;
int size, capacity;
}Кажется, сделать динамический массив меньшими усилиями ну никак нельзя. В самописных версиях иногда вместо двух чиселок также хранят указатели, а size/capacity вычисляют каждый раз. Можно упороться и хранить их в одной
int64 переменной: первые 4 байта на размер, вторые на вместимость. А можно вообще не хранить размер, а сделать ваш вектор нуль-терминированным и считать длину ручками каждый раз. Раз в k случаев такое тоже работает хорошо. Стандартная библиотека предлагает для кастомизации своего вектора подсунуть свой аллокатор вторым шаблонным аргументом. И всё. Больше пользователю ничего не надо. Но ведь есть ещё такая важная характеристика как resize policy -- то, как ваш вектор изменяет свой размер. Как много выделить в первой аллокации? 1, 2, 8? С единицы начинать плохо: после первого push_back почти наверняка придёт второй. С восьми тоже плохо: может у нас вектор объектов, каждый из которых 3Мб, а юзать мы хотим всего для трёх элементов. Да и для двойки свой кейс можно придумать.
Теперь про изменение этого размера. В clang довольно дефолтная схема с умножением на 2 (с оговорками). Но есть ощущение, что умножение на 2 оч плохое решение. Точнее, умножение на любую константу плохо. Когда речь идёт о расширении 8->16, это вполне себе хорошее решение (или нет?🧐без знания размера объектов непонятно). Но когда у вас уже 1кк элементов и вы хотите положить ещё пару штук, расширение 1кк->2кк выглядит как из пушки по воробьям.
И что делать?
Как вариант не расти всегда с одинаковой скоростью. Можно смотреть на текущее кол-во элементов. Можно на занимаемую память. И после выбора какой-то границы, расти в 2 раза до неё и в 1.2 после (понятно что тут стоит покрутить и найти компромисс между занимаемой памятью и временем работы в зависимости от ваших данных и паттерна работы с ними). В go в этом плане примерно так и сделано.
Ещё есть вопрос, уменьшаться ли, если размер стал слишком маленький: если у меня capacity на 1кк, а размер 10, то наверное можно было бы пожаться до этих самых 10, а потом расти по обычным правилам. Вроде как так почти никто не делает. Правда слышал, что в каком-то языке реализовано сжатие, если размер в 4 раза меньше вместимости (сходу не вспомнил где).
В студвекторе из подобного есть метод shrink_to_fit, который уменьшает вместимость до размера. Прошу заметить, что метод этот non-binding, i.e. необязательный. Тут авторы вольны делать что хотят. В clang честно пытаются уменьшиться.
Учитывая, что resize policy это что-то, что нельзя описать одним числом, интересно попробовать придумать, как это могло бы выглядеть в стандартной библиотеке. Что-то такое:
template <
typename T,
typename Allocator = ... ,
template <typename T> Recap = std::twice>
class vector;где Recap может выглядеть как что-то вроде
template <typename T>
size_t MyRecap(size_t sz, size_t cp) {
if (sz * 4 < cp) { return sz; }
if (cp * sizeof(T) > 10 * kBytesInMB) {
return cp * 1.2;
}
return cp * 2;
}Понятно, что всё это можно делать и сейчас, вычисляя новый размер/капасити снаружи и используя
resize/reserve, но менеджерить размеры не изнутри как неестественно. Возвращаясь к теме пооптимизировать вектор, кроме всем известной специализации для булов иногда пишут специализацию для POD-типов, где можно не вызывать конструкторы/деструкторы при различных операциях.
Можно ещё писать контейнеры кусками (вроде дека), чтобы немножко получше жить с аллокациями. Или пробовать хранить мало данных на стеке, прям как sso.
Зачем это всё? А хер его знает. Почти наверняка на практике это вам не пригодится, потому что если вам не хватает
std::vector, то у вас уже либо все остальные проблемы с производительностью решены, либо очень специфическая задача. Но знать полезно.Такие дела.
👍14❤1
#common #python
Не знаю, насколько вам заходит читать про сборщики мусора, но осталось совсем чуть-чуть : )
Собсна python GC.
Стандартный CPython использует подсчёт ссылок и сборщик мусора с поколениями [habr]. Второй необходим в силу того, что стандартный алгоритм подсчёта ссылок не учитывает циклы, возникающие в процессе жизнедеятельности программы.
Всего тут три поколения, для которых можно задавать границы того, сколько сборок мусора должен пережить объект, чтобы переместиться в следующее поколение.
Для разрешения циклов используется факт, что циклы могут возникнуть лишь при использовании различных контейнеров. Для каждого объекта внутри контейнера производится следующая операция: для всех объектов внутри этого контейнера, на которые ссылается зафиксированный объект, количество ссылок уменьшается на 1. Для всех объектов, счётчик ссылок которых остался больше 1, считается, что на них ссылаются объекты снаружи контейнера. Эти объекты кикаем из множества кандидатов на удаление. Также убираем из этого множества все объекты, на которые ссылаются только что убраные и т.д. Оставшиеся во множестве объекты можно удалять.
Подобный подход встречается и в других языках.
Например, в Kotlin/Native (это не обычный котлин, прошу заметить) такой выбор был сделан из-за простоты метода. Однако недавно от подсчёта ссылок решили отказаться, потому что с ним не получается достигнуть достаточной эффективности.
В отличие от подсчёта ссылок, сборщик мусора можно отключать.
Это может быть полезно для эффективности приложения в случае, если вы уверены, что приложение не создаёт циклов, или же готовы на некоторые утечки памяти в угоду скорости. Получается некоторый аналог неудаляющего аллокатора.
Другой интерпретатор python PyPy имеет cборщик мусора с другой моделью поведения. Incminimark -- инкрементальный трассирующий сборщик мусора с двумя поколениями. Основная настройка -- размер молодого поколения (nursery).
Большие объекты создаётся вне поколений.
Когда-то в PyPy вообще было 6 разных сборщиков, но решили от них отказаться. Ещё пару недель назад у меня была ссылка на старую документацию с пруфами, но сегодня по ней уже ничего нет. Вот так быстро мир меняется.
И CPython, и PyPy предоставляют множество возможностей для полуавтоматического управления сборкой мусора. Есть возможности вручную запускать отдельные минорные/мажорные сборки, временно включать/отключать сборщик мусора,
получать подробную информацию о поведении программы при сборке и т.д.
[1]. Как Instagram отключил gc и получил профит.
[2]. Потом что-то покрутили и вернули.
Не знаю, насколько вам заходит читать про сборщики мусора, но осталось совсем чуть-чуть : )
Собсна python GC.
Стандартный CPython использует подсчёт ссылок и сборщик мусора с поколениями [habr]. Второй необходим в силу того, что стандартный алгоритм подсчёта ссылок не учитывает циклы, возникающие в процессе жизнедеятельности программы.
Всего тут три поколения, для которых можно задавать границы того, сколько сборок мусора должен пережить объект, чтобы переместиться в следующее поколение.
Для разрешения циклов используется факт, что циклы могут возникнуть лишь при использовании различных контейнеров. Для каждого объекта внутри контейнера производится следующая операция: для всех объектов внутри этого контейнера, на которые ссылается зафиксированный объект, количество ссылок уменьшается на 1. Для всех объектов, счётчик ссылок которых остался больше 1, считается, что на них ссылаются объекты снаружи контейнера. Эти объекты кикаем из множества кандидатов на удаление. Также убираем из этого множества все объекты, на которые ссылаются только что убраные и т.д. Оставшиеся во множестве объекты можно удалять.
Подобный подход встречается и в других языках.
Например, в Kotlin/Native (это не обычный котлин, прошу заметить) такой выбор был сделан из-за простоты метода. Однако недавно от подсчёта ссылок решили отказаться, потому что с ним не получается достигнуть достаточной эффективности.
В отличие от подсчёта ссылок, сборщик мусора можно отключать.
Это может быть полезно для эффективности приложения в случае, если вы уверены, что приложение не создаёт циклов, или же готовы на некоторые утечки памяти в угоду скорости. Получается некоторый аналог неудаляющего аллокатора.
Другой интерпретатор python PyPy имеет cборщик мусора с другой моделью поведения. Incminimark -- инкрементальный трассирующий сборщик мусора с двумя поколениями. Основная настройка -- размер молодого поколения (nursery).
Большие объекты создаётся вне поколений.
Когда-то в PyPy вообще было 6 разных сборщиков, но решили от них отказаться. Ещё пару недель назад у меня была ссылка на старую документацию с пруфами, но сегодня по ней уже ничего нет. Вот так быстро мир меняется.
И CPython, и PyPy предоставляют множество возможностей для полуавтоматического управления сборкой мусора. Есть возможности вручную запускать отдельные минорные/мажорные сборки, временно включать/отключать сборщик мусора,
получать подробную информацию о поведении программы при сборке и т.д.
[1]. Как Instagram отключил gc и получил профит.
[2]. Потом что-то покрутили и вернули.
👍5❤1
#algo
Хеш-таблицы 1/2.
1. load factor.
Часто говорят, что важно следить за ним. Ведутся жоские споры (у агрошкольников) о том, какие значения норм, а какие нет. В умных книжках часто пишут, что норм 0.5-0.7. В жизни вам может зайти и 0.99, и может больше единицы, и в районе 0.005. Важно понимать, что важен не сам LF, а средняя глубина поиска. Потому что у вас есть коллизии, которые в одном бакете делают 10 элементов, когда вся таблица на 100. Вроде LF 0.1, а работает медленно. И есть удаления. Которые например при открытой адресации просто помечают элементы удалёнными, не ускоряя поиск. Короче за этим надо следить (в случае открытой адресации видимо вас ждут k костылей, в случае закрытой можно считать LF как максимальное кол-во элементов в бакете делить на размер таблицы).
2. Хеш-функция.
Легко можно выбрать хеш-функцию, посмотрев на её бенчмарки (которые где-то там делали авторы и она всё и вся разносит). Но очевидно, что не будет понятно норм/нет, пока вы не померяете на ваших данных. Ну это ладно. Это известный тезис.
А вот какую выбрать? Может я коллизий поменьше хочу. Тогда может что-то из sha-2 или md5? У них, говорят, коллизий мало. Вон они по 128+ бит хеши выдают. ... А ключи же у нас в районе 64х. Получается надо обрезать. И тут уже ничего не понятно, норм оно будет или нет. Ещё и считается долго. Долго это плохо. Надо помнить, что хеш-функция это на каждую операцию. Потому может лучше "плохо" и быстро с помощью crc32, DJB или fnv (главное чтобы не LoseLose). Хотя можно что-то среднее: SuperFastHash, Murmur, CityHash или Robin-hood (или Hopscotch). Вообще про тему скорости хеш-функций можно вспомнить SMHasher, который считает, сколько функция умеет хешировать в секунду и ещё какие-то характеристики. Но тут тоже стоит помнить, что неважно, сколько она хеширует в секунду, если вы собираетесь её юзать для восьмисимвольных строк (надо мерять).
Бывают ли идеальные хеши? Да! Но надо бы знать ключи заранее и делать предпосчёт, зато юзается быстро и без коллизий :) Тут можно взглянуть на gperf и cmph.
Можно вообще выбрать какое-то семейство функций, и время от времени брать функцию из него.
3. Размер.
Можно зафиксировать вашу таблицу на всю жизнь (и жить с открытой адресацией):
Но что-то подсказывает, что такого хватит ненадолго (хотя мб и хватит). Стандартный путь это как вектор (просто элементов для открытой адресации или например листов для закрытой): реаллокация в k раз, чтобы LF получше стал. Как выбирать k смотрите два поста назад. Можно размеры только степенями двойки. Или можно делать статику снаружи и динамику внутри:
Может быть полезно в случае параллельной таблички или если вы локальность цепочки при закрытой адресации хотите.
4. Ключи.
Иногда нужно хранить какую-то мету, само значение (для сравнений каких-нибудь) или значение хеша. В зависимости от того, насколько много занимает ваша структурка, можно выбирать, хранить например всё в одном массивчике или может ключи/значения отдельно, чтобы в кеши хорошо попадало что надо. Тут конечно всё зависит от вашего паттерна доступа.
Кстати забавно, что если объект жирный, можно попробовать хранить первые k байт его представления и сравнивать их. Или можно вообще не сравнивать изначальные ключи с одинаковыми значениями хеша, если вы уверены в хеш-функции или готовы иногда ошибаться.
Хеш-таблицы 1/2.
1. load factor.
Часто говорят, что важно следить за ним. Ведутся жоские споры (у агрошкольников) о том, какие значения норм, а какие нет. В умных книжках часто пишут, что норм 0.5-0.7. В жизни вам может зайти и 0.99, и может больше единицы, и в районе 0.005. Важно понимать, что важен не сам LF, а средняя глубина поиска. Потому что у вас есть коллизии, которые в одном бакете делают 10 элементов, когда вся таблица на 100. Вроде LF 0.1, а работает медленно. И есть удаления. Которые например при открытой адресации просто помечают элементы удалёнными, не ускоряя поиск. Короче за этим надо следить (в случае открытой адресации видимо вас ждут k костылей, в случае закрытой можно считать LF как максимальное кол-во элементов в бакете делить на размер таблицы).
2. Хеш-функция.
Легко можно выбрать хеш-функцию, посмотрев на её бенчмарки (которые где-то там делали авторы и она всё и вся разносит). Но очевидно, что не будет понятно норм/нет, пока вы не померяете на ваших данных. Ну это ладно. Это известный тезис.
А вот какую выбрать? Может я коллизий поменьше хочу. Тогда может что-то из sha-2 или md5? У них, говорят, коллизий мало. Вон они по 128+ бит хеши выдают. ... А ключи же у нас в районе 64х. Получается надо обрезать. И тут уже ничего не понятно, норм оно будет или нет. Ещё и считается долго. Долго это плохо. Надо помнить, что хеш-функция это на каждую операцию. Потому может лучше "плохо" и быстро с помощью crc32, DJB или fnv (главное чтобы не LoseLose). Хотя можно что-то среднее: SuperFastHash, Murmur, CityHash или Robin-hood (или Hopscotch). Вообще про тему скорости хеш-функций можно вспомнить SMHasher, который считает, сколько функция умеет хешировать в секунду и ещё какие-то характеристики. Но тут тоже стоит помнить, что неважно, сколько она хеширует в секунду, если вы собираетесь её юзать для восьмисимвольных строк (надо мерять).
Бывают ли идеальные хеши? Да! Но надо бы знать ключи заранее и делать предпосчёт, зато юзается быстро и без коллизий :) Тут можно взглянуть на gperf и cmph.
Можно вообще выбрать какое-то семейство функций, и время от времени брать функцию из него.
3. Размер.
Можно зафиксировать вашу таблицу на всю жизнь (и жить с открытой адресацией):
std::pair<K, V> data[1024];Но что-то подсказывает, что такого хватит ненадолго (хотя мб и хватит). Стандартный путь это как вектор (просто элементов для открытой адресации или например листов для закрытой): реаллокация в k раз, чтобы LF получше стал. Как выбирать k смотрите два поста назад. Можно размеры только степенями двойки. Или можно делать статику снаружи и динамику внутри:
std::vector<std::pair<K, V>> data[1024];Может быть полезно в случае параллельной таблички или если вы локальность цепочки при закрытой адресации хотите.
4. Ключи.
Иногда нужно хранить какую-то мету, само значение (для сравнений каких-нибудь) или значение хеша. В зависимости от того, насколько много занимает ваша структурка, можно выбирать, хранить например всё в одном массивчике или может ключи/значения отдельно, чтобы в кеши хорошо попадало что надо. Тут конечно всё зависит от вашего паттерна доступа.
Кстати забавно, что если объект жирный, можно попробовать хранить первые k байт его представления и сравнивать их. Или можно вообще не сравнивать изначальные ключи с одинаковыми значениями хеша, если вы уверены в хеш-функции или готовы иногда ошибаться.
👍6 2❤1
#algo
Хеш-таблицы 2/2.
5. Пробинг при открытой адресации.
Пробинг -- то, как вы двигаетесь по таблице в поиске свободного места. Очевидно можно линейный:
6. Доп. требования.
Иногда нужно хранить порядок вставки. Тогда давайте рядом хранить двусвязный лист, а в каждом бакете хранить итератор на начало подцепочки этого бакета в листе и размер подцепочки. Ещё кстати и перебор всех элементов за их количество.
А может ещё вы хотите на каком-то интервале читать значения, тогда вам не хеш-таблица нужна, но об этом попозже : )
7. И немного читов.
-- можно как в LRU-кеше при закрытой адресации найденный где-то в глубине листа элемент перемещать в самое начало, чтобы в следующий раз его поиск прошёл быстрее. В открытой адресации наверное тоже можно посвапать аккуратно с удалённым элементом например, но звучит как что-то, где легко споткнуться.
-- хешировать можно не просто пришедший элемент, а что угодно. Можете хешировать хеш от хеша от хеша. Или первые 8 байт объекта. Или для чисел сначала 228 добавить. Короче вертеть как хочешь, абы хорошо было.
-- вместо листа/вектора при закрытой адресации можете хранить что-то другое. В java например с какого-то момента лист превращается в bst.
[1]. Hash map analysis.
[2]. Extendible hashing.
[3]. Я написал самую быструю хеш-таблицу.
[4]. Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step [видео].
[5]. О хеш-таблицах в Clickhouse [видео].
Хеш-таблицы 2/2.
5. Пробинг при открытой адресации.
Пробинг -- то, как вы двигаетесь по таблице в поиске свободного места. Очевидно можно линейный:
offset[i] = i или i*k. Можно квадратический: offset[i] = i*i или даже alpha*i + beta*i*i. Или double hashing: offset[i] = i*hash2(key). Или cuckoo hashing (ну придумали конечно): для любого ключа проверяем две точки от двух хеш-функций; если есть место, вставляем, иначе выбираем одного из этих двух оккупантов и выселяем его, а на его место наш элемент; аналогично делаем с выселенным элементом; если такое повторяется некоторое количество раз, меняем хеш-функцию и рехеш. Прикольно, что такой приём гарантирует O(1) на поиск. 6. Доп. требования.
Иногда нужно хранить порядок вставки. Тогда давайте рядом хранить двусвязный лист, а в каждом бакете хранить итератор на начало подцепочки этого бакета в листе и размер подцепочки. Ещё кстати и перебор всех элементов за их количество.
А может ещё вы хотите на каком-то интервале читать значения, тогда вам не хеш-таблица нужна, но об этом попозже : )
7. И немного читов.
-- можно как в LRU-кеше при закрытой адресации найденный где-то в глубине листа элемент перемещать в самое начало, чтобы в следующий раз его поиск прошёл быстрее. В открытой адресации наверное тоже можно посвапать аккуратно с удалённым элементом например, но звучит как что-то, где легко споткнуться.
-- хешировать можно не просто пришедший элемент, а что угодно. Можете хешировать хеш от хеша от хеша. Или первые 8 байт объекта. Или для чисел сначала 228 добавить. Короче вертеть как хочешь, абы хорошо было.
-- вместо листа/вектора при закрытой адресации можете хранить что-то другое. В java например с какого-то момента лист превращается в bst.
[1]. Hash map analysis.
[2]. Extendible hashing.
[3]. Я написал самую быструю хеш-таблицу.
[4]. Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step [видео].
[5]. О хеш-таблицах в Clickhouse [видео].
👍13❤1
#list
Пам-пам. Наступила очередь пачки рандомных фактов/ссылок/чего-то ещё.
1. Я думаю, уже чуть ли не каждый видел концепты из C++20. Интересный факт, что в текущем виде (авторства Andrew Sutton'а) они называются concepts lite, потому что первые концепты авторства Bjarne Stroustrup выглядели примерно так:
Ух. Тут и семантические ограничения, и синтаксические. И бог весть знает что ещё. Собсна потому текущие концепты это lite. Потому что была вот такая хардовая версия, которую отказались принимать, потому ту мач сложно. Можете вот тут чуть-чуть почитать.
2. В дополнение к концептам хотят подвезти контракты. Конечно по срокам ничего не понятно. Вот так это примерно будет выглядеть:
Можно посмотреть старый пропозал из любопытства.
Вот текущий. Пока ещё есть вопросы по синтаксису. В качестве расширения в будущем ещё предлагают сделать захват по значению переменных (не оч понял зачем).
3. Это конечно же баян, но вот пример:
В таком виде код не скомпилируется, потому что в нашей структурке нет такой функции, и неизвестно, будет ли она в специализации A. Чтобы сообщить, что g() есть в каждой специализации A можно заюзать самую недооценённую фичу:
4. Давайте посмотрим на сигнатуру
Зачем тут возвращается
Тут тоже компаратор -- любой функтор. Но функция ничего не возвращает : ) Лол короч.
5. Интересный факт, что возможность NRVO (т.к. это необязательная оптимизация) выясняется по анализу скоупа, а не конкретного выполняющегося кода. Можно посмотреть примеры и пояснений в этом и следующем постах на канале (кстати довольно интересном) у @cloudy_district.
6. На летней C++ Zero Cost Conf был доклад Тимура Думлера про real-time вычисления. Интересно было как минимум понять, о чём это. Кстати увидел на том же канале : )
Тут есть интересный факт, что нельзя иметь одновременно равновероятный для всех чисел в промежутке рандом, работающий за детерменированное кол-во шагов. Либо одно, либо второе.
7. Ещё у Жени есть очень крутые статьи на хабре. Кажется, пару штук я даже постил. Из последнего мне очень понравилось про Неклассические контейнеры в C++. Увидел бы я её, когда писал пост про вектор...
Кстати из неё узнал, что в бусте некоторым контейнерам можно задавать политику ресайза. Правда в довольно простом виде, но всё же.
8. Интересная заметка о том, как отлаживать bash-скрипты.
9. Как и зачем писать хороший код. Доклад Григория Петрова на Golang Conf 2019. Кстати довольно известный, я так понял, в локальных кругах чувак. По крайней мере он вёл несколько промежуточных бесед на Highload++ Foundation 2022 в середине мая. Что-то удалось посмотреть, почти всё не удалось посмотреть. Будем полгода ждать записи, отсматривать и рассказывать вам.
Пам-пам. Наступила очередь пачки рандомных фактов/ссылок/чего-то ещё.
1. Я думаю, уже чуть ли не каждый видел концепты из C++20. Интересный факт, что в текущем виде (авторства Andrew Sutton'а) они называются concepts lite, потому что первые концепты авторства Bjarne Stroustrup выглядели примерно так:
concept EqualityComparable<typename T> {
requires constraint Equal<T>;
requires axiom Equivalence_relation<Equal<T>, T>;
// if x == y then for any Predicate p, p(x) == p(y)
template <Predicate p>
axiom Equality(T x, T y, P p) {
x == y => p(x) == p(y);
}
// inequality is the negation of equality
axiom Inequality(T x, T y) {
(x != y) == !(x == y);
}
};Ух. Тут и семантические ограничения, и синтаксические. И бог весть знает что ещё. Собсна потому текущие концепты это lite. Потому что была вот такая хардовая версия, которую отказались принимать, потому ту мач сложно. Можете вот тут чуть-чуть почитать.
2. В дополнение к концептам хотят подвезти контракты. Конечно по срокам ничего не понятно. Вот так это примерно будет выглядеть:
int f(int x, int y)
[[ expects: x > 0 ]] // precondition
[[ expects: y > 0 ]] // precondition
[[ ensures r: r < x + y ]] // postcondition
{
int z = (x - x%y) / y;
[[ assert: z >= 0 ]]; // assertion
return z + y;
}Можно посмотреть старый пропозал из любопытства.
Вот текущий. Пока ещё есть вопросы по синтаксису. В качестве расширения в будущем ещё предлагают сделать захват по значению переменных (не оч понял зачем).
3. Это конечно же баян, но вот пример:
template <class T> struct A {
void g() {}
};
template <class T> struct C : A<T> {
void f() { g(); }
};В таком виде код не скомпилируется, потому что в нашей структурке нет такой функции, и неизвестно, будет ли она в специализации A. Чтобы сообщить, что g() есть в каждой специализации A можно заюзать самую недооценённую фичу:
this:template <class T> struct C : A<T> {
void f() { this->g(); }
// or using A<T>::g;
};4. Давайте посмотрим на сигнатуру
std::for_each:template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f);Зачем тут возвращается
UnaryFunction? Подразумевается, что ваш функтор может быть быть классом с каким-то стейтом, потому нужно вернуть итоговый функтор. Интересно, что в std::sort логика другая:template<class RandomIt, class Compare>
void sort(RandomIt first, RandomIt last, Compare comp);Тут тоже компаратор -- любой функтор. Но функция ничего не возвращает : ) Лол короч.
5. Интересный факт, что возможность NRVO (т.к. это необязательная оптимизация) выясняется по анализу скоупа, а не конкретного выполняющегося кода. Можно посмотреть примеры и пояснений в этом и следующем постах на канале (кстати довольно интересном) у @cloudy_district.
6. На летней C++ Zero Cost Conf был доклад Тимура Думлера про real-time вычисления. Интересно было как минимум понять, о чём это. Кстати увидел на том же канале : )
Тут есть интересный факт, что нельзя иметь одновременно равновероятный для всех чисел в промежутке рандом, работающий за детерменированное кол-во шагов. Либо одно, либо второе.
7. Ещё у Жени есть очень крутые статьи на хабре. Кажется, пару штук я даже постил. Из последнего мне очень понравилось про Неклассические контейнеры в C++. Увидел бы я её, когда писал пост про вектор...
Кстати из неё узнал, что в бусте некоторым контейнерам можно задавать политику ресайза. Правда в довольно простом виде, но всё же.
8. Интересная заметка о том, как отлаживать bash-скрипты.
9. Как и зачем писать хороший код. Доклад Григория Петрова на Golang Conf 2019. Кстати довольно известный, я так понял, в локальных кругах чувак. По крайней мере он вёл несколько промежуточных бесед на Highload++ Foundation 2022 в середине мая. Что-то удалось посмотреть, почти всё не удалось посмотреть. Будем полгода ждать записи, отсматривать и рассказывать вам.
👍6🔥1
#algo
Вероятностные структуры данных 1/2.
Иногда бывают задачи, когда вы готовы пожертвовать точностью каких-то значений ради скорости работы/любой другой характеристики. Пробежимся по самым известным.
1. Фильтр Блума решает задачу проверки значения во множестве. Изначально это какой-то битсет размера m и рядом k хеш-функций. При добавлении элемента во множество с помощью хеш-функций определяется k битов, которые устанавливаются в 1. Теперь при необходимости проверить, находится ли элемент во множестве, считаем от него все хеш-функции и проверяем, правда ли, что все интересующие нас биты установлены в 1. Проблемы: иногда могут возникать ложноположительные срабатывания (элемента нет, а мы ответили, что есть); нельзя удалять элементы (один и тот же бит может быть установлен различными элементами). В таком виде два фильтра легко объединять/пересекать (побитовое ИЛИ/И множеств). В отличие от хеш-таблиц (или чего-то подобного) фильтр блума может являться универсальным множеством (все биты == 1).
Есть разные вариации: вместо 0/1 можно хранить числа и при добавлении инкрементить соответствующие значения, а при удалении декрементить (counting bf); вместо одного counting bf можно хранить n независимых (bitwise почему-то); spectral bf — оптимизированный counting; aging bf, который поддерживает операцию удаления и не хранит старые данные; stable bf, в котором мы рандомно декрементим несколько значений, после чего k значениям устанавливаем максимальные значения (утверждается, что рано или поздно получится достичь какого-то константного кол-ва нулей, магия); A^2 bf — теп memory efficient implementation; ну и конечно более менее умеющий в удаление bf (если кратко, разрешается некоторые участки помечаются как collision-unsafe, и удаление из них не происходит).
Тут ещё можно считать конкретные размеры битсета и кол-во хеш-функций исходя из допустимой пропорции ошибок.
Иногда юзается в кешах или как вспомогательная структура для других сд.
Ещё можно строить key-value хранилище на bf. Если вам нужно что-то вроде
Ещё давайте такой пример. Нам нужно на устройстве пользователя (допотопный мобильный телефон == мало памяти) понимать, звонит нам спамер или нет. Давайте все телефоны из базы спамерских закинем в bf. Чтобы избежать false-positive срабатываний, пройдёмся по всему множеству телефонов (для 10циферных вполне реально) и выпишем рядом с bf номера, на которых мы ошибаемся (их не будет много, порядка единиц при хорошем bf). Теперь если bf говорит, что номер спамерский, проверим, есть ли он в отдельно выписаных. И теперь мы умеем точно отвечать при очень маленьких затратах памяти. Тут можно почитать реальные кейсы.
А ещё можно на них bst строить. А ещё можно в борах юзать. Ух короче.
Основной link.
2. Count-min sketch помогает насчитать частотность различных объектов (например у нас есть бесконечный поток данных, и хочется уметь выдавать примерную частоту появления объектов, причём с очень маленькими затратами на время/память).
Структура очень простая: k хеш-таблиц фиксированного размера (можно одного, можно разных). Изначально все они заполнены нулями. Как только нужно добавить объект, инкрементим соответствующую этому объекту ячейку в каждой хеш-таблице. При запросе узнать частоту объекта, берём значения объекта из каждой хеш-таблицы и считаем минимум. Собственно всё. Понятно, что чем больше хеш-табличек вы берёте, тем ближе ответ будет к реальному, иначе из-за коллизий может получится завышенным.
Ещё если знаем, что элемент есть во множестве, можно удалять.
Только пишется это обычно не как несколько хеш-таблиц, а матрицей + хеш-функцию на каждую строку.
Ещё их легко объединять и (если одно множество строго подмножество второго) вычитать.
Тут (осторожно, скачается пдфка) есть про применение в безопасности, базах данных и даже биоинформатике.
Вероятностные структуры данных 1/2.
Иногда бывают задачи, когда вы готовы пожертвовать точностью каких-то значений ради скорости работы/любой другой характеристики. Пробежимся по самым известным.
1. Фильтр Блума решает задачу проверки значения во множестве. Изначально это какой-то битсет размера m и рядом k хеш-функций. При добавлении элемента во множество с помощью хеш-функций определяется k битов, которые устанавливаются в 1. Теперь при необходимости проверить, находится ли элемент во множестве, считаем от него все хеш-функции и проверяем, правда ли, что все интересующие нас биты установлены в 1. Проблемы: иногда могут возникать ложноположительные срабатывания (элемента нет, а мы ответили, что есть); нельзя удалять элементы (один и тот же бит может быть установлен различными элементами). В таком виде два фильтра легко объединять/пересекать (побитовое ИЛИ/И множеств). В отличие от хеш-таблиц (или чего-то подобного) фильтр блума может являться универсальным множеством (все биты == 1).
Есть разные вариации: вместо 0/1 можно хранить числа и при добавлении инкрементить соответствующие значения, а при удалении декрементить (counting bf); вместо одного counting bf можно хранить n независимых (bitwise почему-то); spectral bf — оптимизированный counting; aging bf, который поддерживает операцию удаления и не хранит старые данные; stable bf, в котором мы рандомно декрементим несколько значений, после чего k значениям устанавливаем максимальные значения (утверждается, что рано или поздно получится достичь какого-то константного кол-ва нулей, магия); A^2 bf — теп memory efficient implementation; ну и конечно более менее умеющий в удаление bf (если кратко, разрешается некоторые участки помечаются как collision-unsafe, и удаление из них не происходит).
Тут ещё можно считать конкретные размеры битсета и кол-во хеш-функций исходя из допустимой пропорции ошибок.
Иногда юзается в кешах или как вспомогательная структура для других сд.
Ещё можно строить key-value хранилище на bf. Если вам нужно что-то вроде
k->v \in [0; 10], давайте держать 11 bf, и по значению v решать, в какой bf добавить ключ. При получении значения пройдёмся по всем bf и в котором нашли ключ, такое и значение.Ещё давайте такой пример. Нам нужно на устройстве пользователя (допотопный мобильный телефон == мало памяти) понимать, звонит нам спамер или нет. Давайте все телефоны из базы спамерских закинем в bf. Чтобы избежать false-positive срабатываний, пройдёмся по всему множеству телефонов (для 10циферных вполне реально) и выпишем рядом с bf номера, на которых мы ошибаемся (их не будет много, порядка единиц при хорошем bf). Теперь если bf говорит, что номер спамерский, проверим, есть ли он в отдельно выписаных. И теперь мы умеем точно отвечать при очень маленьких затратах памяти. Тут можно почитать реальные кейсы.
А ещё можно на них bst строить. А ещё можно в борах юзать. Ух короче.
Основной link.
2. Count-min sketch помогает насчитать частотность различных объектов (например у нас есть бесконечный поток данных, и хочется уметь выдавать примерную частоту появления объектов, причём с очень маленькими затратами на время/память).
Структура очень простая: k хеш-таблиц фиксированного размера (можно одного, можно разных). Изначально все они заполнены нулями. Как только нужно добавить объект, инкрементим соответствующую этому объекту ячейку в каждой хеш-таблице. При запросе узнать частоту объекта, берём значения объекта из каждой хеш-таблицы и считаем минимум. Собственно всё. Понятно, что чем больше хеш-табличек вы берёте, тем ближе ответ будет к реальному, иначе из-за коллизий может получится завышенным.
Ещё если знаем, что элемент есть во множестве, можно удалять.
Только пишется это обычно не как несколько хеш-таблиц, а матрицей + хеш-функцию на каждую строку.
Ещё их легко объединять и (если одно множество строго подмножество второго) вычитать.
Тут (осторожно, скачается пдфка) есть про применение в безопасности, базах данных и даже биоинформатике.
👍8 1
#algo
Вероятностные структуры данных 2/2.
3. HyperLogLog используют, чтобы узнать примерное количество уникальных элементов в мультимножестве.
Тут у нас опять есть k хеш-функций, каждая из которых выдаёт значения от 0 до 2^64-1. Значение хеш-функции это какая-то последовательность из 64 нулей и единиц. Теперь немного помахаем руками. Будем считать, что наши хеш-функции "хорошие", i.e. плюс-минус равномерно распределены (хотя вообще-то мы хотим этого для всех структур). Тогда хеш равновероятно может начаться с нуля и единицы (с p=1/2). Аналогично с 01 с p=1/4, с 001 с p=1/8, 0001 — 1/16 и т.д. Т.е. если мы встретили хеш с префиксом 0000001 (а такое могло произойти только с p=1/128), то мы можем сказать, что в мультимножестве примерно 128 элементов (на больших числах конечно же). Конечно же такая оценка может легко сломаться, если нам попадётся один случайный хеш с длинным префиксом нулей. Для обхода все элементы потока делят на несколько подпотоков (например по очереди отправляют сначала в 1й, потом во 2й, в 3й и в 4й), считают максимальную длину префикса в каждом подпотоке среди всех хеш-функций (например 1, 5, 8, 3) и считают среднее гармоническое на страшные коэффициенты из тервера:
Сейчас есть следующая проблема: если неаккуратно делить на подпотоки, то какой-нибудь частый элемент, дающий маленький хеш, испортит нам все подпотоки. Потому давайте номер подпотока определять по первым k битам хеша. Т.е. если у нас пришёл объект с хешом
Такая структура легко мержится. Предположим у нас есть посчитанный HLL для мультимножества A (массив из n чисел, где n — кол-во подпотоков, а i-е число в нём — позиция самой правой единицы в этом подпотоке) и HLL для B, то смержить их это за O(n) посчитать максимум из двух для каждой ячейки. Соответственно HLL легко параллелится (каждый чанк мультимножества считаем независимо, а потом мержим).
Если немного покумекать-покрутить включения-исключения, можно научиться пересекать/вычитать несколько HLL.
HLL юзается в redis (или статья), или например под капотом для approx_count_distinct в других бд.
4. MinHash используется для оценки мощности пересечения множеств (например будем считать два текста похожими, если множества их слов похожи; можем не храня эти огромные тексты таким примитивным методом задетектить плагиат). Тут вводится коэффициент Жаккара:
Берём k хеш-функций. Считаем значения всех хеш-функций для каждого элемента множества A. Среди значений каждой хеш-функции берём минимум. Получаем массив из k элементов, каждый из которых равен минимальному значению некоторой хеш-функции на этом множестве. Повторяем то же для множества B. И теперь считаем коэффициент Жаккара двух векторов с хешами. Утверждается, что он очень хорошо приближает
Иногда вместо k хеш-функций берут одну, но считают k её минимальных значений. Или одну по разным простым модулям.
Ну и тут link на какие-то юзкейсы.
Вот такую магию люди придумывают, а у нас до сих пор горячую воду отключают на две недели. Мир контрастов.
Вероятностные структуры данных 2/2.
3. HyperLogLog используют, чтобы узнать примерное количество уникальных элементов в мультимножестве.
Тут у нас опять есть k хеш-функций, каждая из которых выдаёт значения от 0 до 2^64-1. Значение хеш-функции это какая-то последовательность из 64 нулей и единиц. Теперь немного помахаем руками. Будем считать, что наши хеш-функции "хорошие", i.e. плюс-минус равномерно распределены (хотя вообще-то мы хотим этого для всех структур). Тогда хеш равновероятно может начаться с нуля и единицы (с p=1/2). Аналогично с 01 с p=1/4, с 001 с p=1/8, 0001 — 1/16 и т.д. Т.е. если мы встретили хеш с префиксом 0000001 (а такое могло произойти только с p=1/128), то мы можем сказать, что в мультимножестве примерно 128 элементов (на больших числах конечно же). Конечно же такая оценка может легко сломаться, если нам попадётся один случайный хеш с длинным префиксом нулей. Для обхода все элементы потока делят на несколько подпотоков (например по очереди отправляют сначала в 1й, потом во 2й, в 3й и в 4й), считают максимальную длину префикса в каждом подпотоке среди всех хеш-функций (например 1, 5, 8, 3) и считают среднее гармоническое на страшные коэффициенты из тервера:
k*(2^(-1) + 2^(-5) + 2^(-8) + 2^(-3))^(-1)
Примерно столько элементов в нашем мультимножестве. Сейчас есть следующая проблема: если неаккуратно делить на подпотоки, то какой-нибудь частый элемент, дающий маленький хеш, испортит нам все подпотоки. Потому давайте номер подпотока определять по первым k битам хеша. Т.е. если у нас пришёл объект с хешом
0010|0010...01, то по первым например 4 битам мы определим, что его подпоток номер 2, а считать позицию первой единицы будем начиная с 5ого бита (в этом примере она равна 3). Если в этом подпотоке уже был элемент с единицей дальше, то просто забываем эту тройку, иначе, если это дальше, чем было до текущего момента, считаем, что 3 у нас самая далёкая. Таким образом частые элементы будут портить один и тот же подпоток.Такая структура легко мержится. Предположим у нас есть посчитанный HLL для мультимножества A (массив из n чисел, где n — кол-во подпотоков, а i-е число в нём — позиция самой правой единицы в этом подпотоке) и HLL для B, то смержить их это за O(n) посчитать максимум из двух для каждой ячейки. Соответственно HLL легко параллелится (каждый чанк мультимножества считаем независимо, а потом мержим).
Если немного покумекать-покрутить включения-исключения, можно научиться пересекать/вычитать несколько HLL.
HLL юзается в redis (или статья), или например под капотом для approx_count_distinct в других бд.
4. MinHash используется для оценки мощности пересечения множеств (например будем считать два текста похожими, если множества их слов похожи; можем не храня эти огромные тексты таким примитивным методом задетектить плагиат). Тут вводится коэффициент Жаккара:
J(A, B) = |пересечения|/|объединения|
Как это работает.Берём k хеш-функций. Считаем значения всех хеш-функций для каждого элемента множества A. Среди значений каждой хеш-функции берём минимум. Получаем массив из k элементов, каждый из которых равен минимальному значению некоторой хеш-функции на этом множестве. Повторяем то же для множества B. И теперь считаем коэффициент Жаккара двух векторов с хешами. Утверждается, что он очень хорошо приближает
J(A, B). Тут можно считать пересечение двух векторов не как множеств, а совпадение по позициям.Иногда вместо k хеш-функций берут одну, но считают k её минимальных значений. Или одну по разным простым модулям.
Ну и тут link на какие-то юзкейсы.
Вот такую магию люди придумывают, а у нас до сих пор горячую воду отключают на две недели. Мир контрастов.
👍8🤯3 1
#list
1. Оч забавный факт, что до C++11 не оставить пустую строку в конце хедера приводило к ub. Препроцессор был настолько глупым, что не догадывался вставить после директивы include перевод строки. Пример:
Прикиньте, если последней строкой шёл комментарий.
2. Немного пропозалов:
- =delete("message"): добавление сообщения при удалении метода/перегрузки;
- добавление saturation arithmetic (это когда все операции ограничены каким-то диапазоном значений);
- разрешить static_assert(false). Сейчас вот такой код не скомпилируется:
Иногда хочется, чтобы работало. Для обхода используют некоторый контекстозависимый trait:
- explicit lifetime managment.
3. Два интересных предложения от рускоязычного сообщества:
- новые математические функции: std::sincos, std::invsqrt, std::rem;
- запретить удаление incomplete классов.
4. На одном из докладов Highload++ чувак из Яндекс Go рассказывал, как они ускорялись. И одним из методов были так называемые hedged requests: сначала вы посылаете запрос (на более подходящую реплику, если у вас есть такая информация), а после него с какой-то небольшой задержкой повторяете (куда-нибудь ещё). В итоге у вас есть несколько запросов, которые выполняются наперегонки. Как только приходит первый ответ, берём его результат и игнорируем остальные. Прикона.
5. Иногда пишут двусвязные списки как xor-листы. В таком случае вместо двух указателей next prev хранится их xor. Таким образом вы можете перемещаться по листу только от начала до конца. Если же вы попали в какую-то ноду случайно, вы не сможете получить адреса её соседей.
1. Оч забавный факт, что до C++11 не оставить пустую строку в конце хедера приводило к ub. Препроцессор был настолько глупым, что не догадывался вставить после директивы include перевод строки. Пример:
Imagine foo.h:
blah blah blah<no newline>
Now bar.c:
#include "foo.h"
#include "grill.h"
Result:
blah blah blah#include "grill.h"Прикиньте, если последней строкой шёл комментарий.
2. Немного пропозалов:
- =delete("message"): добавление сообщения при удалении метода/перегрузки;
- добавление saturation arithmetic (это когда все операции ограничены каким-то диапазоном значений);
- разрешить static_assert(false). Сейчас вот такой код не скомпилируется:
if constexpr (true) {
int x;
} else {
static_assert(false);
}Иногда хочется, чтобы работало. Для обхода используют некоторый контекстозависимый trait:
static_assert(!std::is_same<T, T>::value);- explicit lifetime managment.
3. Два интересных предложения от рускоязычного сообщества:
- новые математические функции: std::sincos, std::invsqrt, std::rem;
- запретить удаление incomplete классов.
4. На одном из докладов Highload++ чувак из Яндекс Go рассказывал, как они ускорялись. И одним из методов были так называемые hedged requests: сначала вы посылаете запрос (на более подходящую реплику, если у вас есть такая информация), а после него с какой-то небольшой задержкой повторяете (куда-нибудь ещё). В итоге у вас есть несколько запросов, которые выполняются наперегонки. Как только приходит первый ответ, берём его результат и игнорируем остальные. Прикона.
5. Иногда пишут двусвязные списки как xor-листы. В таком случае вместо двух указателей next prev хранится их xor. Таким образом вы можете перемещаться по листу только от начала до конца. Если же вы попали в какую-то ноду случайно, вы не сможете получить адреса её соседей.
👍15❤1
#highload
Вы почти наверняка знаете про LRU (Last Recently Used) кеш. Давайте на всякий случай вспомним. Мы храним до k некоторых значений, при этом поддерживая время последнего обращения. При появлении в кеше k+1-ого элемента удаляем элемент, к которому обращение было "давнее" всего. Делается это просто: держим лист ключей (в начале самые молодые по времени обращения элементы, в конце самые старые) и мапку из ключа в итератор этого листа. При необходимости удалить, просто кикаем из листа последний элемент. При необходимости обновить время обращения к элементу, получаем из мапки итератор на ноду листа и переставляем её в самое начало. Базово так. У Антона Полухина есть интересный доклад про оптимизацию LRU. Речь о том коде, который является базой для кешей в половине случаев, которые я у нас вижу. Да, вы может видели мнения, что LRU -- неудачник среди кешей, но часто его хватает.
Чуть менее примитивным вариантов является сегментированный LRU. По факту это несколько LRU. Сначала кладём в первый кеш. При запросе элемента из него, перемешаем во второй. И т.д. Эти кеши условно можно называть cold, warm и hot (в случае трёх частей). Как будто поколения в сборщиках мусора.
Прокачаным LRU является 2Q. Тут есть три части. Первая (in) + вторая (out) -- обычная FIFO очередь. При добавлении в кеш элемент кладётся в in. Со временем он переходит в out (который обычно значительно больше in). Третья часть -- hot cache (например LRU). Элементы из in вытесняются в out.
Если запрашивается элемент, который в in, с ним ничего не происходит. Если который в out, он перемещается в hot. Если элемент был вытеснен в out и не запрашивается, он просто удаляется из кеша при достижении какого-то размера.
Юзается в postgres.
Хотя когда-то в postgres использовался ARC (adaptive replacement cache), но там тёрки за патенты с IBM. Суть в том, что у вас есть две части: L1 (для recently used элементов) и L2 (для частоиспользуемых элементов). Каждая L часть состоит тоже из двух частей: из самих элементов кеша (T) и из удалённых из него элементов (B/ghosts lists), которые тем не менее ещё трекаются. Если элемент из B1 опять появляется в кеше, он возвращается в T1. При этом один элемент из T2 вытесняется в B2. И наоборот. Такое взаимодействие вот.
LFU -- least frequently used. У каждого элемента есть счётчик количества обращений. Элемент с наименьшим значением счётчика удаляется. Если элементов с минимальным значением несколько, по порядку FIFO. Понятно, что очень просто сделать такое с помощью std::set, но есть алгоритм, который позволяет вставку, lookup и удаление за O(1) (опять хеш-таблица и связанные списки специфического вида).
Есть ещё несколько разных стратегий:
- MRU: удаляем последний использованный элемент (бережём старые элементы);
- Mid point LRU: сегментированный LRU с двумя частями (cold, hold);
- MQ: сегментированный LRU, в котором запоминается место, из которого элемент вытесняется (если знание о месте из кеша не пропало). При повторном появлении в кеше, он вставляется в это место. Так можно быстрее прогревать кеш при циклической ротации элементов.
- алгоритм Белади: отбрасывать из кеша ту информацию, которая не понадобится в будущем дольше всего. Так как в общем случае невозможно предсказать, когда в следующий раз пригодится именно эта информация, то на практике подобная реализация невозможна (в общем случае). Можно вычислить опытным путём, после чего сравнить такую реализацию с текущим кешом;
- pseudo-LRU: оптимизированный LRU, позволяющий тратить гораздо меньше памяти.
=============================
Заканчиваю вот универ. Чувствую, как появляется больше времени на (пора)ботать, пусть и последние несколько месяцев ничего почти по нему не делал. Хочется наконец найти силы и методично разобрать огромный накопившийся беклог + попробовать в пару начинаний. Непонятно, как оно получится и получится ли вообще, но если будут предприниматься какие-то действия, обязательно поделюсь.
Вы почти наверняка знаете про LRU (Last Recently Used) кеш. Давайте на всякий случай вспомним. Мы храним до k некоторых значений, при этом поддерживая время последнего обращения. При появлении в кеше k+1-ого элемента удаляем элемент, к которому обращение было "давнее" всего. Делается это просто: держим лист ключей (в начале самые молодые по времени обращения элементы, в конце самые старые) и мапку из ключа в итератор этого листа. При необходимости удалить, просто кикаем из листа последний элемент. При необходимости обновить время обращения к элементу, получаем из мапки итератор на ноду листа и переставляем её в самое начало. Базово так. У Антона Полухина есть интересный доклад про оптимизацию LRU. Речь о том коде, который является базой для кешей в половине случаев, которые я у нас вижу. Да, вы может видели мнения, что LRU -- неудачник среди кешей, но часто его хватает.
Чуть менее примитивным вариантов является сегментированный LRU. По факту это несколько LRU. Сначала кладём в первый кеш. При запросе элемента из него, перемешаем во второй. И т.д. Эти кеши условно можно называть cold, warm и hot (в случае трёх частей). Как будто поколения в сборщиках мусора.
Прокачаным LRU является 2Q. Тут есть три части. Первая (in) + вторая (out) -- обычная FIFO очередь. При добавлении в кеш элемент кладётся в in. Со временем он переходит в out (который обычно значительно больше in). Третья часть -- hot cache (например LRU). Элементы из in вытесняются в out.
Если запрашивается элемент, который в in, с ним ничего не происходит. Если который в out, он перемещается в hot. Если элемент был вытеснен в out и не запрашивается, он просто удаляется из кеша при достижении какого-то размера.
Юзается в postgres.
Хотя когда-то в postgres использовался ARC (adaptive replacement cache), но там тёрки за патенты с IBM. Суть в том, что у вас есть две части: L1 (для recently used элементов) и L2 (для частоиспользуемых элементов). Каждая L часть состоит тоже из двух частей: из самих элементов кеша (T) и из удалённых из него элементов (B/ghosts lists), которые тем не менее ещё трекаются. Если элемент из B1 опять появляется в кеше, он возвращается в T1. При этом один элемент из T2 вытесняется в B2. И наоборот. Такое взаимодействие вот.
LFU -- least frequently used. У каждого элемента есть счётчик количества обращений. Элемент с наименьшим значением счётчика удаляется. Если элементов с минимальным значением несколько, по порядку FIFO. Понятно, что очень просто сделать такое с помощью std::set, но есть алгоритм, который позволяет вставку, lookup и удаление за O(1) (опять хеш-таблица и связанные списки специфического вида).
Есть ещё несколько разных стратегий:
- MRU: удаляем последний использованный элемент (бережём старые элементы);
- Mid point LRU: сегментированный LRU с двумя частями (cold, hold);
- MQ: сегментированный LRU, в котором запоминается место, из которого элемент вытесняется (если знание о месте из кеша не пропало). При повторном появлении в кеше, он вставляется в это место. Так можно быстрее прогревать кеш при циклической ротации элементов.
- алгоритм Белади: отбрасывать из кеша ту информацию, которая не понадобится в будущем дольше всего. Так как в общем случае невозможно предсказать, когда в следующий раз пригодится именно эта информация, то на практике подобная реализация невозможна (в общем случае). Можно вычислить опытным путём, после чего сравнить такую реализацию с текущим кешом;
- pseudo-LRU: оптимизированный LRU, позволяющий тратить гораздо меньше памяти.
=============================
Заканчиваю вот универ. Чувствую, как появляется больше времени на (пора)ботать, пусть и последние несколько месяцев ничего почти по нему не делал. Хочется наконец найти силы и методично разобрать огромный накопившийся беклог + попробовать в пару начинаний. Непонятно, как оно получится и получится ли вообще, но если будут предприниматься какие-то действия, обязательно поделюсь.
👍16 1
#list
Чуть-чуть разгребаюсь с беклогом (со скоростью черепахи конечно). Вот вам пачка ссылок почитать:
1. У Жени (автора @cxx95) есть вот такая замечательная статья про создание нового ключевого слова в C++: defer. И не читами с деструкторами и макросами, а честным патчем кланга.
2. В посте выше обнаружилась ссылка на статью про историю LLVM (местами правда со спорными тезисами). Довольно интересно.
3. The true price of virtual functions in C++.
Тут можно почитать о том, какие проблемы возникают при использовании виртуальных функций (с пруфами в виде бенчмарков) и о том, как с этим попытаться побороться, если вам оч надо. Стало чуть понятнее, почему иногда вместо наследования упарываются с CRTP.
4. Хороший (хотя может и немного скучноватый) доклад про рандом в плюсах. Тут разбирается их концептуальное устройство, какие-то проблемы с реализацией своего рандома и немного best practice.
По проблемам есть такой пример: пусть e -- какой-то random engine. Мы хотим сэмулировать бросок кубика:
Но такое решение имеет несколько проблем, из-за которых кубик становится не совсем честным. Предлагаю посмотреть : )
5. Ещё у меня есть список интересных вопросов на stackoverlfow, которыми хотелось бы поделиться (по чуть-чуть конечно🙃):
- про branch prediction;
- почему
- про использование
====================
В Москве видел машины убера с надписью Uber -> ub. Надеюсь, это не о качестве их кода.
Чуть-чуть разгребаюсь с беклогом (со скоростью черепахи конечно). Вот вам пачка ссылок почитать:
1. У Жени (автора @cxx95) есть вот такая замечательная статья про создание нового ключевого слова в C++: defer. И не читами с деструкторами и макросами, а честным патчем кланга.
2. В посте выше обнаружилась ссылка на статью про историю LLVM (местами правда со спорными тезисами). Довольно интересно.
3. The true price of virtual functions in C++.
Тут можно почитать о том, какие проблемы возникают при использовании виртуальных функций (с пруфами в виде бенчмарков) и о том, как с этим попытаться побороться, если вам оч надо. Стало чуть понятнее, почему иногда вместо наследования упарываются с CRTP.
4. Хороший (хотя может и немного скучноватый) доклад про рандом в плюсах. Тут разбирается их концептуальное устройство, какие-то проблемы с реализацией своего рандома и немного best practice.
По проблемам есть такой пример: пусть e -- какой-то random engine. Мы хотим сэмулировать бросок кубика:
e() % 6 + 1Но такое решение имеет несколько проблем, из-за которых кубик становится не совсем честным. Предлагаю посмотреть : )
5. Ещё у меня есть список интересных вопросов на stackoverlfow, которыми хотелось бы поделиться (по чуть-чуть конечно🙃):
- про branch prediction;
- почему
std::reduce требует для операции коммутативность (мы когда-то писали свой и не налагали такие требования, потому возник вопрос; спойлер: для векторизации);- про использование
auto вместе с приватными типами. ====================
В Москве видел машины убера с надписью Uber -> ub. Надеюсь, это не о качестве их кода.
👍8
#pub #algo
Пиу-пау.
Если вам так же как и мне нравится узнавать интересные (пусть и бесполезные) идеи, которые вы никогда на практике не заюзаете, предлагаю почитать мою новую статью про нестандартные (но простые) структуры данных.
https://habr.com/ru/post/673776/
Пиу-пау.
Если вам так же как и мне нравится узнавать интересные (пусть и бесполезные) идеи, которые вы никогда на практике не заюзаете, предлагаю почитать мою новую статью про нестандартные (но простые) структуры данных.
https://habr.com/ru/post/673776/
👍17👎1🔥1
#common
(сейчас будет капитан очевидность, но получите)
Я очень люблю рефакторить.
Прям кипятком ссусь. Сегодня в любом более менее взрослом проекте с несколькими разработчиками огромное количество кода. Всю смысловую нагрузку уже давно невозможно держать в голове. Ты можешь понимать какие-то основные подходы написанного. Может даже детали реализации (зачем именно так что-то было сделано). Но помнить абсолютно всё нереально. И это проблема.
Размеры текущей кодовой базы, в которой вы ежедневно сидите, только растут. С каждой новой таской вы приходите в пусть даже уже и знакомые (хотя часто всё равно нет) места и тратите время на то, чтобы понять вводные, которые вам код предоставляет. Иногда вы пытаетесь разобраться даже в своём собственном коде (я вот недавно наткнулся на непонятное место, которое накодил полгода назад; пришлось покопаться и вспомнить, что меня сподвигло на такой код). Рассогласованность действий разработчиков тоже подкидывает хаоса. Кто-то может сидеть в конкретном сервисе довольно продолжительное время, а кто-то мимокрокодилом пролетел и закоммитил что-то некрасивое/объёмное просто потому что он не знал, что принято/можно сделать лучше. После какого-то времени начинаешь замечать, что одни действия делаются по-разному, что где-то используются обобщения для сокращения количества кода, а где-то нет. С приходом новых участников возникает всё больше вопросов, почему тут сделано так, а тут нет. На это всё тратится время. Очень драгоценное время.
Чтобы со всем этим бороться, можно:
1. Помнить про ревью.
Делать хорошее ревью это важно. Не только с точки зрения корректности логики, но и удачности технической реализации. У меня иногда прохождение ревью занимает столько же, сколько и сама задача (и это не потому что я говнокодер, честно; а потому что ревьюеры обладает более широкой экспертизой и оставляют замечательные комментарии).
Отдавать на ревью не одному человеку. Понятно, что кто-то понимает конкретно в этом месте больше, чем другие. У меня недавно был кейс, когда один коллега оставил неплохое, но небольшое ревью на пр, а чувак из соседней команды, которому пр попался совершенно случайно, накидал комментов ещё на два целых рабочих дня. И всё по делу. Понятно, что тут стоит найти компромисс между полезностью и потраченным количеством человекачасов, но если есть возможность, почему ей не воспользоваться.
Если у вас нет практики ревью (такое бывает, да), всеми силами пытаться её ввести или хотя бы просить ревью на свои задачи. Особенно поначалу это позволяет многому научиться.
2. Тратить время не только на продуктовые задачи, но и на технические. Если в планы на следующий отрезок времени/спринт/самое ближайшее будущее у вас есть возможность запланировать/сделать/убедить коллег в необходимости что-нибудь зарефакторить, постарайтесь этим заняться. Это облегчит жизнь не только вам, но и всем разработчикам вокруг.
3. Ради бога, если вы в процессе решения задачи увидели что-то, что можно позже исправить, запишите это. А то некрасивый код так и останется неисправленным, пусть и обнаруженным. Всё помнить нереально и неполезно.
Сейчас речь идёт именно о (не)больших технических изменениях, потому что крупных продуктовых рефакторингов за свой небольшой опыт я пока не увидел (может вот сейчас наблюдаю за стартом такого процесса). Там всё примерно так же, только чуточку сложнее.
Есть кстати движение под названием Dead Code Society, которое продвигает идею выпиливания неиспользуемого кода. Слышал, что в больших компаниях есть похожие внутренние штуки (например довольно сильное в Meta). У нас тоже вроде что-то такое имеется, но как там у ребят успехи, хз. Свечку не держал.
UPD.
Рад, что вы пользуетесь реакциями кроме 👍 : )
Но их особенно приятно видеть.
(сейчас будет капитан очевидность, но получите)
Я очень люблю рефакторить.
Прям кипятком ссусь. Сегодня в любом более менее взрослом проекте с несколькими разработчиками огромное количество кода. Всю смысловую нагрузку уже давно невозможно держать в голове. Ты можешь понимать какие-то основные подходы написанного. Может даже детали реализации (зачем именно так что-то было сделано). Но помнить абсолютно всё нереально. И это проблема.
Размеры текущей кодовой базы, в которой вы ежедневно сидите, только растут. С каждой новой таской вы приходите в пусть даже уже и знакомые (хотя часто всё равно нет) места и тратите время на то, чтобы понять вводные, которые вам код предоставляет. Иногда вы пытаетесь разобраться даже в своём собственном коде (я вот недавно наткнулся на непонятное место, которое накодил полгода назад; пришлось покопаться и вспомнить, что меня сподвигло на такой код). Рассогласованность действий разработчиков тоже подкидывает хаоса. Кто-то может сидеть в конкретном сервисе довольно продолжительное время, а кто-то мимокрокодилом пролетел и закоммитил что-то некрасивое/объёмное просто потому что он не знал, что принято/можно сделать лучше. После какого-то времени начинаешь замечать, что одни действия делаются по-разному, что где-то используются обобщения для сокращения количества кода, а где-то нет. С приходом новых участников возникает всё больше вопросов, почему тут сделано так, а тут нет. На это всё тратится время. Очень драгоценное время.
Чтобы со всем этим бороться, можно:
1. Помнить про ревью.
Делать хорошее ревью это важно. Не только с точки зрения корректности логики, но и удачности технической реализации. У меня иногда прохождение ревью занимает столько же, сколько и сама задача (и это не потому что я говнокодер, честно; а потому что ревьюеры обладает более широкой экспертизой и оставляют замечательные комментарии).
Отдавать на ревью не одному человеку. Понятно, что кто-то понимает конкретно в этом месте больше, чем другие. У меня недавно был кейс, когда один коллега оставил неплохое, но небольшое ревью на пр, а чувак из соседней команды, которому пр попался совершенно случайно, накидал комментов ещё на два целых рабочих дня. И всё по делу. Понятно, что тут стоит найти компромисс между полезностью и потраченным количеством человекачасов, но если есть возможность, почему ей не воспользоваться.
Если у вас нет практики ревью (такое бывает, да), всеми силами пытаться её ввести или хотя бы просить ревью на свои задачи. Особенно поначалу это позволяет многому научиться.
2. Тратить время не только на продуктовые задачи, но и на технические. Если в планы на следующий отрезок времени/спринт/самое ближайшее будущее у вас есть возможность запланировать/сделать/убедить коллег в необходимости что-нибудь зарефакторить, постарайтесь этим заняться. Это облегчит жизнь не только вам, но и всем разработчикам вокруг.
3. Ради бога, если вы в процессе решения задачи увидели что-то, что можно позже исправить, запишите это. А то некрасивый код так и останется неисправленным, пусть и обнаруженным. Всё помнить нереально и неполезно.
Сейчас речь идёт именно о (не)больших технических изменениях, потому что крупных продуктовых рефакторингов за свой небольшой опыт я пока не увидел (может вот сейчас наблюдаю за стартом такого процесса). Там всё примерно так же, только чуточку сложнее.
Есть кстати движение под названием Dead Code Society, которое продвигает идею выпиливания неиспользуемого кода. Слышал, что в больших компаниях есть похожие внутренние штуки (например довольно сильное в Meta). У нас тоже вроде что-то такое имеется, но как там у ребят успехи, хз. Свечку не держал.
UPD.
Рад, что вы пользуетесь реакциями кроме 👍 : )
Но их особенно приятно видеть.
👍15🤡10👏4🤔4👌3😱1 1
#highload
Взаимодействие фронта с беком в средней REST-экосистеме выглядит как какое-то количество эндпоинтов (ручек) со стороны бекенда, которые дёргаются фронтом. На бекенде может считаться, что хорошо бы разделить логику одного частого действия на некоторое количество действий поменьше, чтобы сделать вызовы быстрее, а систему более переиспользуемой. С другой стороны, клиенту может быть неудобно пользоваться подобными решениями, т.к. вместо похода в одну ручку необходимо сделать несколько походов в разные, что очевидно повышает вероятность ошибки.
Потому возникло такое понятия, как backend for frontend: набор ручек как прослойка между бекендом и фронтендом, которые объединяют популярные (и логические) связки вызовов ручек.
Давайте поймём, какие плюсы мы получаем от внедрения такого подхода.
Во-первых, при обычных походах из клиента сразу в бекенд, каждый поход это передача данных по сети из внешнего мира внутрь экосистемы сервиса. На это тратится время пользователя. При использовании bff первоначальный запрос уходит в bff, после чего все запросы от него к сервисам бекенда делаются внутри (например) одного дата-центра (что вообще-то почти бесплатно).
Во-вторых, при использовании bff появляется возможность отдавать не просто результаты GET/POST/других запросов, из которых необходимо на устройстве пользователя согласно некоторой логике создавать отображение, а отдавать готовую html-разметку для тупого её отображения (мб с js-скриптами). Вы разгружаете клиент: ему достаточно сделать всего один запрос (ещё слышал, что индексирующие роботы при поиске делают лишь один запрос по адресу, потому ранжирование будет более точным и на основе всех данных, а не куска, который вернула всего одна ручка). На bff можно удалять ненужные данные, пришедшие с бекенда (информацию, ненужную для отображения/удалить ненужные данные при пагинации). Соответственно можно и менять status code ответа (т.к. правила для бекенда возвращать не 200 не всегда удобны для фронтенда и 200 им вполне сгодилось бы).
Выглядит конечно, как бекенд-разработчики делают свои дела где-то в сторонке, а фронтенд вынужден в этом жить и что-то изобретать. Может и так. Но если всем норм, поч нет : )
Взаимодействие фронта с беком в средней REST-экосистеме выглядит как какое-то количество эндпоинтов (ручек) со стороны бекенда, которые дёргаются фронтом. На бекенде может считаться, что хорошо бы разделить логику одного частого действия на некоторое количество действий поменьше, чтобы сделать вызовы быстрее, а систему более переиспользуемой. С другой стороны, клиенту может быть неудобно пользоваться подобными решениями, т.к. вместо похода в одну ручку необходимо сделать несколько походов в разные, что очевидно повышает вероятность ошибки.
Потому возникло такое понятия, как backend for frontend: набор ручек как прослойка между бекендом и фронтендом, которые объединяют популярные (и логические) связки вызовов ручек.
Давайте поймём, какие плюсы мы получаем от внедрения такого подхода.
Во-первых, при обычных походах из клиента сразу в бекенд, каждый поход это передача данных по сети из внешнего мира внутрь экосистемы сервиса. На это тратится время пользователя. При использовании bff первоначальный запрос уходит в bff, после чего все запросы от него к сервисам бекенда делаются внутри (например) одного дата-центра (что вообще-то почти бесплатно).
Во-вторых, при использовании bff появляется возможность отдавать не просто результаты GET/POST/других запросов, из которых необходимо на устройстве пользователя согласно некоторой логике создавать отображение, а отдавать готовую html-разметку для тупого её отображения (мб с js-скриптами). Вы разгружаете клиент: ему достаточно сделать всего один запрос (ещё слышал, что индексирующие роботы при поиске делают лишь один запрос по адресу, потому ранжирование будет более точным и на основе всех данных, а не куска, который вернула всего одна ручка). На bff можно удалять ненужные данные, пришедшие с бекенда (информацию, ненужную для отображения/удалить ненужные данные при пагинации). Соответственно можно и менять status code ответа (т.к. правила для бекенда возвращать не 200 не всегда удобны для фронтенда и 200 им вполне сгодилось бы).
Выглядит конечно, как бекенд-разработчики делают свои дела где-то в сторонке, а фронтенд вынужден в этом жить и что-то изобретать. Может и так. Но если всем норм, поч нет : )
👍2👎1