Конкурентные ассоциативные контейнеры
TRANSCRIPT
Concurrent mapsConcurrent maps
LIBCDS
Максим Хижинский, C++ Russia 2015
Операции:● insert( key, value )● erase( key )● find( key )
План:● Concurrent maps изнутри● Управление памятью в lock-free структурах● Lock-free ordered list и concurrent maps на его основе● Немного о деревьях
Hash tableHash table
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X Xk1
k
k
k k
k
kk
k
k
2
3
4 5
6
7
8
9
T[7]
T[i] = std::hash( key ) % 7
Списокколлизий
Striped mapStriped map
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X X
k1
k
k
k k
k
kk
k
k
2
3
4 5
6
7
8
9
T[7]
T[i] = std::hash( key ) % 7
Striped mapStriped map
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X X
k1
k
k
k k
k
kk
k
k
2
3
4 5
6
7
8
9
T[7]
T[i] = std::hash( key ) % 7
Lock[7]
Striped map: rehashStriped map: rehash
LIBCDS
Максим Хижинский, C++ Russia 2015
1. for ( i = 0; i < L; i++ ) Lock[i].lock();
2. N := 2 * N;
3. rehash T
4. for ( i := L - 1; i >=0; --i ) Lock[i].unlock();
L — constInitial: L = N
Rehash: N := 2N
Striped map: rehashStriped map: rehash
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X
k1k
k
k k kkk
kk
2
3
4 5 67
89
T[8]
Lock[i] = std::hash( key ) % 4
Lock[4]
7
T[j] = std::hash( key ) % 8
Striped mapStriped map
LIBCDS
Максим Хижинский, C++ Russia 2015
Lock[i] = std::hash( key ) % L
Lock[L]
α β γ δ
Tree search
Lock-free hash tableLock-free hash table
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X Xk1
k
k
k k
k
kk
k
k
2
3
4 5
6
7
8
9
T[8]
Lock-freeсписок
коллизий
7
k10
Lock-free ordered listLock-free ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
Операции:
● insert( node )● erase( key )● find( key )
template <class T>struct node { std::atomic<node*> next_; T data_;};
H T52 8
Lock-free примитивы:● atomic load/store● atomic compare-and-swap (CAS)
CAS — compare-and-swapCAS — compare-and-swap
LIBCDS
Максим Хижинский, C++ Russia 2015
template <typename T>bool CAS( T * pAtomic, T expected, T desired )atomically { if ( *pAtomic == expected ) { *pAtomic = desired; return true;
} else return false;};
Lock-free list: insertLock-free list: insert
LIBCDS
Максим Хижинский, C++ Russia 2015
H T52 8
3
H T52 8
3
3. prev->next_.CAS( next, new_node )
1. find insert position for key 3
2. new_node.next_.store( next )
H T52 8prev next
new_node
Lock-free list: eraseLock-free list: erase
LIBCDS
Максим Хижинский, C++ Russia 2015
1. find key 3
2. prev->next_.CAS( found, next )
H T52 8
prev
3
found
H T52 83
Проблема: параллельный insert
next
Lock-free list: insert/eraseLock-free list: insert/erase
LIBCDS
Максим Хижинский, C++ Russia 2015
A: find key 3
H T52 8prev
3found
B: find insert pos for key 4
iprev inextA: erase key 3
H T52 83
prev->next_.CAS( found, next )
next
B: insert key 4
H T52 83
4
iprev->next_.CAS( inext, new_item )
local vars
Marked pointerMarked pointer
LIBCDS
Максим Хижинский, C++ Russia 2015
[ T.Harris, 2001 ]
Двухфазное удаление:● Логическое удаление — помечаем элемент● Физическое удаление — исключаем элемент
В качестве метки используем младший бит указателя
Lock-free list: marked pointerLock-free list: marked pointer
LIBCDS
Максим Хижинский, C++ Russia 2015
H T52 8prev
3foundiprev inext
nextA: eraseB: insert
A: Logical deletion - mark item found
H T52 83
found->next_.CAS( next, next | 1 )B: iprev->next_.CAS( inext, new_item ) - failed!!!
A: Physical deletion - remove item found
H T52 83
prev->next_.CAS( found, next )
Lock-free list: problemsLock-free list: problems
LIBCDS
Максим Хижинский, C++ Russia 2015
H T52 8
prev
3
foundiprev inext
nextA: eraseB: insert
iprev->next_.CAS( inext, new_item )
prev->next_.CAS( found, next )
local vars
Вдруг уже удалены?..
Lock-free list: problemsLock-free list: problems
LIBCDS
Максим Хижинский, C++ Russia 2015
Проблемы:
● Защита локальных данных — когда элемент можно безопасно удалить?
● ABA-проблема
ABA-проблемаABA-проблема
LIBCDS
Максим Хижинский, C++ Russia 2015
52
prev
3
found next
Thread A: erase(3) Thread B
52 3
erase(3); erase(5)
2 3insert(4)
Heap
new node(4) alloc
delete
42
preempted...
42
prev found next
5
addr(3) == addr(4)
prev->next_.CAS( found, next ) - success!!!
2 мусор
SMRSMR
LIBCDS
Максим Хижинский, C++ Russia 2015
Проблемы:● Защита локальных данных — когда элемент можно
безопасно удалить?● ABA-проблема
Решение: Safe memory reclamation (SMR)● Tagged pointers● Hazard Pointers● User-space RCU
Tagged pointersTagged pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
pointer tag
prev->next_.dwCAS( found, <next.ptr, prev->next_.tag + 1> )
template <class T>struct tagged_ptr { T * ptr; uintptr_t tag;};
Требует dwCAS — не везде есть Решает только ABA-проблему
Освободить память нельзя, нужен free-list
[ boost.lock-free ]
H T52 8prev
3found next
Tagged pointers: historyTagged pointers: history
LIBCDS
Максим Хижинский, C++ Russia 2015
ABA-проблема характерна только для CAS
Архитектуры процессоров
LL/SC: ● IBM PowerPC● MIPS● ARM
➢ LL — load linked➢ SC — store conditional
bool weak_CAS( T * ptr, T expected, T desired ){ T cur = LL( ptr ); return cur == expected && SC( ptr, desired );}
Эмуляция LL/SC на CAS — намного труднее
CAS: ● x86, amd64● Sparc● Itanium
С++11 — только CAS
Hazard pointersHazard pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
✔ Использует только атомарные чтение/запись Защищает только локальные ссылки✔ Размер массива отложенных (готовых к
удалению) элементов ограничен сверху Перед работой с указателем его следует
объявить как hazard
решает ABA-проблему Физическое удаление элементов
Hazard pointersHazard pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
H T52 8prev
3found next
erase( Key k ) { hp_guard h1 = get_guard(); hp_guard h2 = get_guard();retry: node * prev = Head; do { node * found = h2.protect( prev->next_); if ( found->key == k ) if (prev->next_.CAS( found, found->next_)) { hp_retire( found ); return true; } else goto retry; h1 = h2; prev = found; } while ( found->key < k ); return false; }
Распределяем HP (TLS)
Защищаем элемент
Удаляем элемент
Hazard pointersHazard pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
P – thread count
Thread 0
Thread HP Manager
0
1
…
K - 1
HP[K]0
1
2
…
R - 1
Retired[R]
Hazard Pointer Singleton
Thread 1
Thread P - 1
K = 4 R = 2 KP
<K,P, R> : R > K * P
Hazard PointersHazard Pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
Объявление Hazard Pointer'а – защита локальной ссылки
class hp_guard {
void * hp;
// ...
};
T * hp_guard::protect(
std::atomic<T*>& what) {
T * t;
do {
hp = t = what.load(std::memory_order_relaxed);
} while (t != what.load(std::memory_order_acquire));
return t;
}
0
1
…
K - 1
HP[K]
Hazard PointersHazard Pointers
LIBCDS
Максим Хижинский, C++ Russia 2015
Удаление элемента
void hp_retire( T * what ) {
push what to current_thread.Retired array
if ( current_thread.Retired is full )
hp.Scan( current_thread );
}
void hp::Scan() {
void * guarded[K*P] = union HP[K] for all P thread;
foreach ( p in current_thread.Retired[R] )
if ( p not in guarded[] )
delete p;
}
<K,P, R> : R > K * P
012…
R - 1
Retired[R]
01…
K - 1
HP[K]
User-space Read-Copy UpdateUser-space Read-Copy Update
LIBCDS
Максим Хижинский, C++ Russia 2015
✔ RCU — метод синхронизации: RCU.lock() / RCU.unlock()
✔ Разработан для почти-read-only данных (map, set)✔ Очень легкие read-side lock✔ Удаление элемента — ожидание окончания эпохи
решает ABA-проблему Физическое удаление элементов
RCU.lock(): ничего не блокируетОбъявляет, что поток входит в текущую RCU-эпоху
User-space RCUUser-space RCU
LIBCDS
Максим Хижинский, C++ Russia 2015
Map.find( ... );
Set.insert( ... );
Map.find( ... );
Map.erase( ... )...
Thread 1
Set.find( ... );
Map.insert( ... );
Set.find( ... );
Set.insert( ... );
Thread N
Эпоха 1
RCU.sync() - ждем, пока все потоки покинут эпоху 1
Set.find( ... );
Map.insert( ... );
Set.find( ... );
Set.insert( ... );
... Map.erase;
Map.find( ... );
Set.insert( ... );
Map.find( ... );
++Эпоха
Эпоха 2
Lock-free hash tableLock-free hash table
LIBCDS
Максим Хижинский, C++ Russia 2015
0 1 2 3 4 5 6
X X Xk1
k
k
k k
k
kk
k
k
2
3
4 5
6
7
8
9
T[8]
Lock-freecписок:HP/RCU
+ marked pointers
7
k10
Hash table + Lock-free ordered list
No rehashing
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
Rehashing приводит к перераспределению элементов между buckets
0 1
2
4
6
3
5
0 1 2 3
2 354
6
Key % 2 Key % 4
Вместо того, чтобы перемещать элементы между buckets, будем перемещать buckets между элементами
[ Nir Shavit, 2003 ]
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13T[0]
T[1]
k iSentinel node Regular node
N = 2 size() = 4
Load factor L: size() / N ≤ L
Если L = 2, то вставка нового элемента приводит к увеличениюhash table
Hash table
Lock-free ordered list
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13T[0]
T[1]
k iSentinel node Regular node
N = 4
Hash table
Insert
T[2] = null
T[3] = null
10
T[i]: i = hash % N
10 % 4 = 2 — T[2] не инициализирован
Нужно вставить новый sentinel node
Куда вставлять?..
2
10
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13T[0]
T[1]
k iSentinel node Regular node
N = 4
Hash table
Insert
T[2] = null
T[3] = null
10 2
Split bucket
Bucket = hash % 2m + 1
Parent_bucket Parent_bucket + 2m
= 10 % 4
10 % 4, m = 12
Parent_bucket = 0
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13T[0]
T[1]
k iSentinel node Regular node
N = 4
Hash table
Insert
T[2] = null
T[3] = null
10 2
Split bucket
Bucket = hash % 2m + 1
Parent_bucket Parent_bucket + 2m
= 10 % 4
10 % 4, m = 12
Parent_bucket = 0
0000 1000 0010 0001 1001 1101
0010
Арифметикапо модулю
2p
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13T[0]
T[1]
k iSentinel node Regular node
N = 4
Hash table
Insert
T[2] = null
T[3] = null
10 2
Split bucket
Bucket = hash % 2m + 1
Parent_bucket Parent_bucket + 2m
= 10 % 4
10 % 4, m = 12
Parent_bucket = 0
0000 0001 0100 1000 1001 1011
0100
Инвертируем
порядок бит!
Отсортирован!
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
template <class Key, class T>struct split_list_node { Key key; uint key_hash; // = std::hash(key) uint shah; // = invert( key_hash ) - для сортировки T data;};
Уже есть!!!
parent_bucket
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13T[0]
T[1]
k iSentinel node Regular node
Hash table
Insert
T[2] = null
T[3] = null
2
0000 0 0001 1 0100 1 1000 0 1001 1 1011 1
Надо различать sentinel и regular node.
msb(key_hash) = 1 — regular → lsb( shah ) = 1
msb(key_hash) = 0 — sentinel →lsb( shah ) = 0
0100 0
parent_bucket
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13
T[0]
T[1]
k iSentinel node Regular node
Hash table
Insert
T[2]
T[3] = null
2
0000 0 0001 1 0100 1 1000 0 1001 1 1011 1
0100 0
2
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
0 8 2 1 9 13
T[0]
T[1]
k iSentinel node Regular node
Hash table
Insert
T[2]
T[3] = null
0000 0 0001 1 0100 1 1000 0 1001 1 1011 10100 0
2
10
0101 1
0101 1
10
Bucket = 10 % 4 = 2
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
Удаление — как из обычного lock-free списка
Sentinel node никогда не удаляются
1: Logical deletion - mark item
52 83
found->next_.CAS( next, next | 1 )
2: Physical deletion - unlink item
52 83
Split-ordered listSplit-ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
T[0]
T[1]
. . . .
T[N-1]
T[N]
seg0
seg1
. . . .
null
Segment[M]
T[0]
T[1]
. . . .
T[N-1]
T[N]
T[0]
T[1]
. . . .
T[N-1]
T[N]
Статический массив
сегментов
Сегментная организация hash table
Сегмент – старшие биты хеша T[i] – младшие N бит хеша
Динамическое расширение
Lock-free ordered listLock-free ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
X10 15 23 34 5542
Complexity O(N)
Что ещё можно сделать из lock-free ordered list?
✔ Простой hash map — без расширения ✔ Split-ordered list
find(34)
Lock-free ordered listLock-free ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
X10 15 23 34 5542
Complexity O(N/2)
X
Lock-free ordered listLock-free ordered list
LIBCDS
Максим Хижинский, C++ Russia 2015
X10 15 23 34 5542
Complexity O(N/4)
X
X
Skip listSkip list
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
Head Tail
[ W.Pugh, 1990 ]
Skip listSkip list
LIBCDS
Максим Хижинский, C++ Russia 2015
XXXXXXXX10 15 23 34 5542
23
Tower
h = 3
Вероятностная структура данных:
P[ h == 1 ] = 1/2
P[ h == k ] = 1/2k, 0 < k < 32
h = lsb( rand() )
O(log(N))
Skip list: insertSkip list: insert
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
Head Tail
Поиск позиции: формируем массив prev[]
Skip list: insertSkip list: insert
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
Head Tail
Связываем в список на уровне 0
Skip list: insertSkip list: insert
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
Head Tail
Связываем в список на уровне 1, 2, …снизу вверх
Skip list: eraseSkip list: erase
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
Head Tail
Двухфазное: 1. Logical deletion — markсверху вниз
Skip list: eraseSkip list: erase
LIBCDS
Максим Хижинский, C++ Russia 2015
X
X
X
X
X
X
X
X10 15 23 34 5542
Head Tail
Фаза 2. Physical deletion — unlinkсверху вниз
Skip listSkip list
LIBCDS
Максим Хижинский, C++ Russia 2015
Особенности:
Отсортированный контейнер get_max(), get_min() Можно использовать как priority queue Сложность поиска: O(log N)
Hazard Pointer: для max height = 32 требует min 64 hazard pointer'ов Требует эффективной реализации tower
Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
«Прямой» lock-free подход приводит к очень сложнымалгоритмам.Non-blocking подход дает менее сложные алгоритмы
Routing nodes - только ключи
Leaf nodes - данные
Leaf-oriented tree
Основная цель: lock-free / wait-free find()
Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
10
5 20
3015
27 45
Thread A: erase(15)Thread B: erase(27)
10
5 20
3015
27 45
Error!!!Достижимый
узел
Конкурентное CAS-based удаление
CAS
CAS
Должно быть
Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
10
5 20
3015
27 45
Thread A: erase(27)Thread B: insert(29)
10
5 20
3015
27
45
Error!!!Недостижимые
узлы
Конкурентное CAS-based удаление + вставка
CAS
29
29
Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
Routing node
Left child Right child
State
Key
IFlag DFlag
MarkClean (default)
Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
10
5 20
3015
27 45
Insert 291 30.State.CAS( Clean, IFlag)
10
5 20
3015
27
4527
29
2 Insert30.left.CAS( ) 27 27
10
5 20
3015
27
4527
29
3 30.State.CAS( IFlag, Clean )
Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
10
5 20
3015
27 45
Erase 27
1 20.State.CAS( Clean, DFlag)
10
5 20
3015
27 45
2 Mark 30 30.State.CAS( Clean, Mark)
10
5 20
3015
27 45
3 Delete 20.right.CAS( ) 30 45
10
5 20
3015
27 45
4 20.State.CAS(Dflag, Clean)
Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
10
5 20
3015
27 45
Thread A: erase(15)Thread B: erase(27)
10
5 20
3015
27 45
Error!!!Достижимый
узел
Конкурентное CAS-based удаление
CAS
CAS
Должно быть
Конкуренция
Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
10
5 20
3015
27 45
Thread A: erase(15)Thread B: erase(27)
Конкурентное CAS-based удаление
A
BA
10
5 20
3015
27 45
A
A
Выиграл A
10
5 20
3015
27 45
AВыиграл B
B
B
B: повтор поиска 27
A: unflag + повтор поиска 15
Binary search treeBinary search tree
LIBCDS
Максим Хижинский, C++ Russia 2015
find() - lock-freeinsert(), erase() - non-blocked
Характеристики:
Сложность:
O(log(N)) — для случайных ключейO(N) — в худшем случае
libcdslibcds
LIBCDS
Максим Хижинский, C++ Russia 2015
https://github.com/khizmax/libcds
Все это, а также многое другое, можно найти здесь:
PerformancePerformance
LIBCDS
Максим Хижинский, C++ Russia 2015
Intel Dual Xeon X5670 2.93 GHz 12 cores 24 threads / 24 GB RAM
Concurrent mapsConcurrent maps
Максим Хижинский, C++ Russia 2015
Спасибо за внимание[email protected]
https://github.com/khizmax/libcds