Immutable collections
.NET
Under the hood
Dmitry Ivanov, JetBrains
Качество кода
1. Тесты всех видов, test coverage, property-based тесты
2. Preconditions for public methods, assertions for private
3. Compile-type checks: JetBrains Annotations ([NotNull],
[Pure]), roslyn analyzers
Чем мы займемся сегодня?
1. Поймём, где в вашем продукте иммутабельные
коллекции могут принести пользу и каким образом
2. Посмотрим на известные неизменяемые структуры
3. Изучим внутреннее устройство и потестируем
производительность реализаций в .NET
I. Мотивация
Mutable objects
class Point {
int X {get; set;}
int Y {get; set;}
Point(int x, int y) { X = x; Y = y; }
void IncreaseX(int xOffset) { X += xOffset; }
void IncreaseY(int yOffset) { Y += xOffset; }
}
Mutable objects
class Point {
int X {get; set;}
int Y {get; set;}
Point(int x, int y) { X = x; Y = y; }
void IncreaseX(int xOffset) { X += xOffset; }
void IncreaseY(int yOffset) { Y += xOffset; }
}
Shared state
class Point {
int X {get; set;}
int Y {get; set;}
Point(int x, int y) { … }
void IncreaseX(int xOffset) { … }
void IncreaseY(int yOffset) { … }
int GetHashCode() {…}
bool Equals(object other) {…}
}
Point
PolygonCircle
Contexts
Thread
Pool
Immutable
class Point { //could be struct
readonly int X;
readonly int Y;
Point(int x, int y) { X = x; Y = y; }
Point IncreaseX(int xOffset) => new Point(x + xOffset, y);
Point IncreaseY(int yOffset) => new Point(x, y + yOffset);
int GetHashCode() {…}
bool Equals(object other) {…}
}
Threading models
Message passingShared state
STA threads,
SynchronizationContext
BeginInvoke(), Invoke()
Single thread
UI Thread
Shared state
await
Use only one CPU core!
set 1
set 2
get != 2
012
Pessimistic locking
a1
a2
b1
Thread A Thread B
b2
lock
Readers-writer lock
a1
a2
b1
Thread A Thread B
b2
rwlock
Readers-writer lock
a1
a2
b1
Thread A Thread B
b2
rwlock
Thread C
c1
c2
Readers-writer lock
a1
a2
b1
Thread A Thread B
b2
rwlock
Thread C
c1
c2
Content model rwlock
start
calc
UI Thread Background threads
merge
Content model rwlock
start
calc
UI Thread Background threads
merge
write activity
Content model rwlock
start
calc
UI Thread Background threads
merge
write activity interrupts here
Should check CancellationToken
Content model rwlock
start
calc
UI Thread Background threads
merge
write activity interrupts here
Should check CancellationToken
1. Write unresponsiveness
2. Lots of checks
3. Lots of Assert(Read/Write)
4. Bad intermediate state
Immutable model
State
Activity A Activity B
State A
State A’
State A’
State B
State B’
Immutable model
State
Activity A Activity B
State A
State A’
State A’
State B
State B’
Software transactional memory
Transaction 1 Transaction 2
read A
write B
Memory’
read B
write C
Memory
II. Иммутабельность
Immutable collections
System.Collections.Immutable
• Array
• Stack
• Queue
• List
• Dictionary (+ Sorted)
• Set (+Sorted)
• READ methods without changes:
• Get,
• Count,
• GetEnumerator
• …
• WRITE methods returns collection type
instead of void
• Add
• Insert
• Remove
• Clear
• …
Copy-on-write
class CopyOnWriteArray<T>
{
private readonly T[] _store;
private CopyOnWriteArray(T[] store) { _store = store; }
//read access doesn't copy
public T Get(int index) => _store[index];
//write access always copy everything
public CopyOnWriteArray<T> Set(int index, T item)
{
var newStore = new T[_store.Length];
_store.CopyTo(newStore, 0); //copy content
newStore[index] = item;
return new CopyOnWriteArray<T>(newStore);
}
}
Copy-on-write
class ImmutableArray<T>
{
public static ImmutableArray<T> Empty =
new ImmutableArray(new T[0]);
private readonly T[] _store;
private ImmutableArray(T[] store) { _store = store; }
//read access doesn't copy
public T Get(int index) => _store[index];
//write access always copy everything
public ImmutableArray<T> Set(int index, T item) { … }
public ImmutableArray<T> Add(T item) { … }
public ImmutableArray<T> AddRange(T[] items) { … }
}
Immutable stack
System.Collections.Immutable
• Array
• Stack
• Queue
• List
• Dictionary (+ Sorted)
• Set (+Sorted)
Algorithmic complexity is the same as mutable stack’s one
Immutable stack
System.Collections.Immutable
Thread safe
Not linearizable
Immutable collections
System.Collections.Immutable
Thread safe
Not linearizable
Could be linearized by lock-free algorithm
Immutable to lock-free
SomeImmutableCollection collection = <initial>;
…
do {
int old = ds;
var new = transform(old);
} while (!CAS(ref collection, new, old)) //Compare-and-swap
Lock-free stack
class LockFreeStack<T> {
ImmutableStack<T> immutable = <initial>;
void Push(T item) {
do {
var old = immutable;
var new = old.Push(item); //new stack created!
} while (Interlocked.CompareExchange(ref s, new, old) != old)
}
}
ImmutableInterlocked.Push<T>(ref ImmutableStack<T> stack, T value)
Immutable queue
System.Collections.Immutable
• Array
• Stack
• Queue
• List
• Dictionary (+ Sorted)
• Set (+Sorted)
Immutable queue
System.Collections.Immutable
• Array
• Stack
• Queue
• List
• Dictionary (+ Sorted)
• Set (+Sorted)
Immutable queue
System.Collections.Immutable
• Array
• Stack
• Queue
• List
• Dictionary (+ Sorted)
• Set (+Sorted)
Algorithmic complexity is the same as mutable queue’s one
Immutable map & set
System.Collections.Immutable
• Array
• Stack
• Queue
• List
• Dictionary (+ Sorted)
• Set (+Sorted)
5
6
1 4 7 8
2
Immutable map & set
System.Collections.Immutable
• Array
• Stack
• Queue
• List
• Dictionary (+ Sorted)
• Set (+Sorted)
5
6
1 4 7 8
2
3
Immutable map & set
System.Collections.Immutable
• Array
• Stack
• Queue
• List
• Dictionary (+ Sorted)
• Set (+Sorted)
5
6
1 4 7 8
2
5
2
4
3
Algorithmic for Get, Add, Remove: O(Log(N))
Immutable list
System.Collections.Immutable
• Array
• Stack
• Queue
• List
• Dictionary (+ Sorted)
• Set (+Sorted)
5
6
1 4 7 8
2
5
2
4
3
Can’t be implemented like this because indexes shift right after insertion
Rope
8
gi
abc
def
6
9
var s = “abc” + “def” + “gi”
s.Insert(7, “h”)
ghi
Rope
8
gi
abc
def
6
9
var s = “abc” + “def” + “gi”
s.Insert(7, “h”)
3
g h
2 i
Rope
8
gi
abc
def
6
9
var s = “abc” + “def” + “gi”
s.Insert(7, “h”)
3
g h i
Rope
d,5
e,1
a,1 c,1
b,3
var s = “abcde”.Rope()
System.Collections.Immutable
• Array
• Stack
• Queue
• List
• Dictionary (+ Sorted)
• Set (+Sorted)
Algorithmic for Add, Remove: O(Log(N)) (Faster than mutable List)
More Ropes
var s = `some 1MB string`
16 refs|counts
16 refs|counts 16 refs|counts 16 refs|counts…
256 bytes block = 16*8*2 = 128 chars
4 levels = 128*16*16*16 = 512K chars16 refs|counts
128 chars 128 chars 128 chars
III. Практика
Резюме
1. Теперь вы знаете почему выбор менее
производительной структуры данных может увеличить
производительность вашей команды
2. Теперь вы легко выберете самую подходящую
системную коллекцию и проверите её скорость
3. Теперь вы можете легко написать свою супер-
оптимальную иммутабельную коллекцию. Но это не
точно…
Что почитать?
• .NET Framework - Immutable Collections by Hadi Brais, March 2017
• Ropes: Theory and practice - IBM by Amin Ahmad, February 2008
• STM: https://groups.csail.mit.edu/tds/papers/Shavit/ShavitTouitou-podc95.pdf
Вопросы и ответы
Вопрошающий
Любознательный
Посетитель
Лектор
blocking queue
(maxsize = 1)

.NET Fest 2018. Дмитрий Иванов. Иммутабельные структуры данных в .NET: зачем нужны и как устроены