Skip to content

Commit 42911b5

Browse files
committed
Merge branch 'optimisation'
Conflicts: lib/src/map_impl.dart
2 parents 384c28c + 0e1e5da commit 42911b5

34 files changed

Lines changed: 1355 additions & 1470 deletions

README.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ The project is forked from
99
*Persistent* data structure is an immutable structure; the main difference with standard data structures is how you 'write' to them: instead of mutating
1010
the old structure, you create the new, independent, (slightly) modified copy of it. Typical examples of commonly used Persistent structures are String (in Java, Javascript, Python, Ruby) or Python's Tuple or Java's BigDecimal. [(Not only)](http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey) we believe such concept could be beneficial also for other data structures such as Maps, Lists/Vectors, Sets.
1111

12-
var couple = new PersistentMap.fromMap({'father': 'Homer', 'mother': 'Marge'});
12+
var couple = new PMap.fromMap({'father': 'Homer', 'mother': 'Marge'});
1313
// do not (and can not) modify couple anymore
1414
var withChild = couple.assoc('boy', 'Bart');
1515
print(couple); // {mother: Marge, father: Homer}
@@ -28,8 +28,8 @@ got a 'great' idea, that instead of just computing response length he also mutat
2828
Finally, they work as you'd expect. How cool is this:
2929

3030
// deeply persist the structure of Maps and Lists
31-
var a = persist({[1,2]: 'tower', [1,3]: 'water'});
32-
var b = persist({[1,2]: 'tower', [1,3]: 'water'});
31+
PMap a = persist({[1,2]: 'tower', [1,3]: 'water'});
32+
PMap b = persist({[1,2]: 'tower', [1,3]: 'water'});
3333
assert(a==b);
3434
// kids, don't try this with standard List, it ain't going to work
3535
print(a[persist([1, 2])]); // prints 'tower'
@@ -48,21 +48,24 @@ One possible workaround would be to JSONize entries to string and use such strin
4848
Fast copying or equality done right are nice features, but this is not the only selling point here. Having different ways how to copy (shallow, deep) objects or how to compare them (== vs. equals in Java) introduces new complexity. Even if you get used to it, it still takes some part of your mental capabilities and can lead to errors.
4949

5050
### Structure sharing
51-
var map1 = persist({'a': 'something', 'b': bigMap});
52-
var map2 = a.assoc('a', 'something completely different');
51+
PMap map1 = persist({'a': 'something', 'b': bigMap});
52+
PMap map2 = a.assoc('a', 'something completely different');
5353
Suppose you are interested, whether map1['b'] == map2['b']. Thanks to structure sharing, this is O(1) operation, which means it is amazingly fast - no need to traverse through the bigMap. Although it may sound unimportant at the first glance, it is what really enables fast caching of complex functions. Also, this is the reason, why [Om](https://github.com/swannodette/om/) framework is MUCH faster than Facebooks [React](http://facebook.github.io/react/).
5454

5555
## And what is the prize for this all
56-
Short version: size and speed. Although structure sharing makes the whole thing much more effective than naive copy-it-all approach, Persistents still are slower and bigger than their mutable counterparts. Following numbers illustrate, how much less efficient (in terms of speed or memory consumption) are Persistent data structures when benchmarking either on DartVM or Dart2JS on Node:
56+
In the 2.0 release, we optimized memory consumption such that the only penalty for using Persistent
57+
comes at lower speed. Although structure sharing makes the whole thing much more effective than naive
58+
copy-it-all approach, Persistents are still slower than their mutable counterparts (note however, that on
59+
the other hand, some operations runs significantly faster, so its hard to say something conclusive
60+
here). Following numbers illustrate, how much slow are Persistent data structures when benchmarking either on DartVM
61+
or Dart2JS on Node (the numbers are quite independent of the structure size):
5762

58-
* DartVM memory: 2.5
59-
* Dart2JS memory: 6
6063
* DartVM read speed: 2
61-
* DartVM write speed: 2.5
62-
* Dart2JS read speed: 2
63-
* Dart2JS write speed: 4
64+
* DartVM write speed: 12 (5 by using Transients)
65+
* Dart2JS read speed: 3
66+
* Dart2JS write speed: 14 (6 by using Transients)
6467

65-
The good part of the story is, that these numbers are not getting worse, as the Map grows - you get such performance even for Maps with tens of megabytes of data stored within them.
68+
Although the factors are quite big, the whole operation is still very fast and it probably won't be THE bottleneck which would slow down your app.
6669

6770
Some [advanced topics](https://github.com/vacuumlabs/persistent/wiki/Advanced-topics).
6871

benchmark/map_memory/makefile

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ run: build/map_memory.js
3838

3939
@echo "$$INTRO"
4040

41-
@echo -n "DartVM persistent: "
42-
@dart --old_gen_heap_size=1024 map_memory.dart ${template_size} persistent > build/temp.out 2>/dev/null
43-
@tail -1 build/temp.out
44-
45-
@echo -n "DartVM map: "
46-
@dart --old_gen_heap_size=1024 map_memory.dart ${template_size} map > build/temp.out 2>/dev/null
47-
@tail -1 build/temp.out
41+
# @echo -n "DartVM persistent: "
42+
# @dart --old_gen_heap_size=1024 map_memory.dart ${template_size} persistent > build/temp.out 2>/dev/null
43+
# @tail -1 build/temp.out
44+
#
45+
# @echo -n "DartVM map: "
46+
# @dart --old_gen_heap_size=1024 map_memory.dart ${template_size} map > build/temp.out 2>/dev/null
47+
# @tail -1 build/temp.out
4848

4949
@echo -n "NodeJS persistent: "
5050
@ (! node --max-old-space-size=1024 build/map_memory.js ${template_size} persistent > build/temp.out) 2>/dev/null
@@ -54,5 +54,9 @@ run: build/map_memory.js
5454
@ (! node --max-old-space-size=1024 build/map_memory.js ${template_size} map > build/temp.out) 2>/dev/null
5555
@tail -1 build/temp.out
5656

57+
@echo -n "NodeJS hashmap: "
58+
@ (! node --max-old-space-size=1024 build/map_memory.js ${template_size} hashmap > build/temp.out) 2>/dev/null
59+
@tail -1 build/temp.out
60+
5761
rm -f build/temp.out
5862

benchmark/map_memory/map_memory.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
library map_memory;
88

99
import 'package:vacuum_persistent/persistent.dart';
10+
import 'dart:collection';
1011

1112
Map template = {};
1213

@@ -15,15 +16,16 @@ List data = [];
1516

1617
var creators = {
1718

18-
"persistent": () => new PersistentMap.fromMap(template),
19+
"persistent": () => new PMap.fromMap(template),
1920

2021
"transient": (){
21-
var res = new TransientMap();
22+
var res = new TMap();
2223
template.forEach((k, v) => res.doAssoc(k, v));
2324
return res;
2425
},
2526

2627
"map": () => new Map.from(template),
28+
"hashmap": () => new HashMap.from(template)
2729
};
2830

2931
void run(int template_size, String mode) {

benchmark/map_speed/benchmarks.dart

Lines changed: 40 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,66 +8,63 @@ part of map_bench;
88

99
class WriteBenchmark extends BenchmarkBase{
1010

11-
final int size;
12-
final BenchmarkInterface object;
11+
final Map<num, num> sample;
12+
BenchmarkInterface object;
13+
final dynamic factory;
1314

15+
void setup(){
16+
object = factory();
17+
}
1418

15-
WriteBenchmark(size, object, name):
16-
size = size,
17-
object = object,
18-
super("Writing $name($size)");
19-
19+
WriteBenchmark(this.sample, this.factory):super('Writing');
2020

2121
void run(){
22-
23-
object.create();
24-
25-
for (int i = 0; i < size; i++) {
26-
object.assoc("key$i", "foo");
27-
object.assoc("key$i", "bar");
28-
}
29-
30-
object.save();
31-
for (int i = 0; i < size; i++) {
32-
object.delete("key$i");
33-
}
34-
35-
object.restore();
36-
for (int i = 0; i < size; i++) {
37-
object.delete("key$i");
22+
for (var size in this.sample.keys) {
23+
for (var j=0; j<this.sample[size]; j++){
24+
object = factory();
25+
for (var val in ["foo", "bar", "baz", "woo", "hoo", "goo", "wat"]){
26+
for (int i = 0; i < size; i++) {
27+
object.assoc(i*i, val);
28+
}
29+
}
30+
for (int i = 0; i < size; i++) {
31+
object.delete(i*i);
32+
}
33+
}
3834
}
3935
}
4036
}
4137

4238

4339
class ReadBenchmark extends BenchmarkBase{
4440

45-
final int size;
46-
final BenchmarkInterface object;
47-
41+
final Map<num, num> sample;
42+
Map<num, List<BenchmarkInterface>> objects = new Map();
43+
final dynamic factory;
4844

49-
ReadBenchmark(size, object, name):
50-
size = size,
51-
object = object,
52-
super("Reading $name($size)");
45+
ReadBenchmark(this.sample, this.factory):super('Reading');
5346

5447
void setup(){
55-
56-
object.create();
57-
58-
for (int i = 0; i < size; i++) {
59-
object.assoc("key$i", "foo");
60-
}
48+
this.sample.forEach((size, count){
49+
objects[size] = [];
50+
for (int j=0; j<count; j++){
51+
BenchmarkInterface object = factory();
52+
objects[size].add(object);
53+
for (int i = 0; i < size; i++) {
54+
object.assoc(i*i, "foo");
55+
}
56+
}
57+
});
6158
}
6259

6360
void run(){
64-
65-
for (int i = size * 2; i >= 0; i--) {
66-
object.get("key$i");
67-
}
68-
69-
for (int i = 0; i <= size * 2; i++) {
70-
object.get("key$i");
61+
for (var size in this.sample.keys) {
62+
for (var j=0; j<this.sample[size]; j++){
63+
BenchmarkInterface object = objects[size][j];
64+
for (int i = size; i >= 0; i--) {
65+
object.get(i*i);
66+
}
67+
}
7168
}
7269
}
7370
}

benchmark/map_speed/interface.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ part of map_bench;
88

99
abstract class BenchmarkInterface<K, V>{
1010

11-
void create();
1211
void assoc(K key, V value);
1312
void get(K key);
1413
void delete(K key);

benchmark/map_speed/interface_impl.dart

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
part of map_bench;
88

99
class PersistentMapInterface<K, V>
10-
extends EncapsulatingInterface<K, V, PersistentMap<K, V>>{
10+
extends EncapsulatingInterface<K, V, PMap<K, V>>{
1111

12-
create() => object = new PersistentMap<K, V>();
12+
PersistentMapInterface(){
13+
this.object = new PMap<K, V>();
14+
}
1315

1416
assoc(K key, V value) =>
1517
object = object.assoc(key, value);
@@ -23,9 +25,11 @@ class PersistentMapInterface<K, V>
2325

2426

2527
class TransientMapInterface<K, V>
26-
extends EncapsulatingInterface<K, V, TransientMap<K, V>>{
28+
extends EncapsulatingInterface<K, V, TMap<K, V>>{
2729

28-
create() => object = new PersistentMap<K, V>().asTransient();
30+
TransientMapInterface(){
31+
this.object = new PMap<K, V>().asTransient();
32+
}
2933

3034
assoc(K key, V value) =>
3135
object.doAssoc(key, value);
@@ -45,7 +49,9 @@ class TransientMapInterface<K, V>
4549
class StandardMapInterface<K, V>
4650
extends EncapsulatingInterface<K, V, Map<K, V>>{
4751

48-
create() => object = new Map<K, V>();
52+
StandardMapInterface(){
53+
object = new Map<K, V>();
54+
}
4955

5056
assoc(K key, V value) =>
5157
object[key] = value;
@@ -91,7 +97,7 @@ class LinkedListInterface<K, V>
9197
while (it.isCons) {
9298
Cons<Pair<K, V>> cons = it.asCons;
9399
Pair<K, V> elem = cons.elem;
94-
if (elem.first == key) {
100+
if (elem.fst == key) {
95101
builder.add(new Pair<K, V>(key, value));
96102
return builder.build(cons.tail);
97103
}
@@ -107,7 +113,7 @@ class LinkedListInterface<K, V>
107113
while (it.isCons) {
108114
Cons<Pair<K, V>> cons = it.asCons;
109115
Pair<K, V> elem = cons.elem;
110-
if (elem.first == key) return;
116+
if (elem.fst == key) return;
111117
it = cons.tail;
112118
}
113119
}

benchmark/map_speed/map_speed.dart

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,62 @@ library map_bench;
88

99
import 'package:vacuum_persistent/persistent.dart';
1010
import 'package:benchmark_harness/benchmark_harness.dart';
11+
import 'dart:math';
1112

1213
part 'benchmarks.dart';
1314
part 'interface.dart';
1415
part 'interface_impl.dart';
1516

16-
var interfaces = {
17-
//"LinkedList": () => new LinkedListInterface(),
17+
Map interfaces = {
1818
"PersistentMap": () => new PersistentMapInterface(),
1919
"TransientMap": () => new TransientMapInterface(),
20-
"StandartMap": () => new StandardMapInterface(),
21-
//"CopyMap": () => new CopyMapInterface(),
20+
"Map": () => new StandardMapInterface(),
2221
};
2322

24-
void main() {
25-
26-
for (int n in [1,10,100,1000,10000]) {
27-
for (String name in interfaces.keys){
28-
new ReadBenchmark(n, interfaces[name](), name).report();
29-
}
30-
}
23+
int times = 10;
3124

32-
for (int n in [1,10,100,1000,10000]) {
33-
for (String name in interfaces.keys){
34-
new WriteBenchmark(n, interfaces[name](), name).report();
25+
void main() {
26+
var config = [
27+
{'name': 'Write',
28+
'creator': ((sample, factory) => (new WriteBenchmark(sample, factory))),
29+
// 'sizes': [{10000: 1}, ],
30+
'sizes': [{500:60, 1000: 30, 1500: 20, 3000: 10}],
31+
},
32+
{
33+
'name': 'Read',
34+
'creator': ((sample, factory) => (new ReadBenchmark(sample, factory))),
35+
// 'sizes': [{10000: 1}, ],
36+
'sizes': [{500:60, 1000: 30, 1500: 20, 3000: 10}],
37+
}
38+
];
39+
var result = {};
40+
config.forEach((conf){
41+
String mode = conf['name'];
42+
var creator = conf['creator'];
43+
for (Map sample in conf['sizes']) {
44+
var res = {};
45+
var dev = {};
46+
interfaces.forEach((k,v){
47+
res[k] = 0;
48+
dev[k] = 0;
49+
});
50+
for (int i=0; i<times; i++){
51+
for (String name in interfaces.keys) {
52+
var meas = creator(sample, interfaces[name]).measure();
53+
res[name] += meas;
54+
dev[name] += meas*meas;
55+
}
56+
}
57+
for (String name in interfaces.keys) {
58+
res[name] /= times;
59+
dev[name] /= times;
60+
dev[name] = sqrt(dev[name] - res[name] * res[name]);
61+
}
62+
for (String name in interfaces.keys) {
63+
var _dev = 2*(res[name]*dev['Map']+res['Map']*dev[name])/res['Map']/res['Map']/sqrt(times);
64+
print('${mode} ${name} sample ${sample}: ${res[name]/res['Map']} '+
65+
'+- ${_dev} (${res[name]} us)');
66+
}
3567
}
36-
}
37-
68+
});
3869
}

benchmark/mori_speed/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
compiled.js
3+
!mori_speed.js

benchmark/mori_speed/makefile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
all: clean run
2+
3+
init:
4+
npm up
5+
6+
compiled.js: mori_speed.js
7+
node_modules/closurecompiler/bin/ccjs mori_speed.js > compiled.js
8+
9+
run: compiled.js
10+
node compiled.js
11+
12+
clean:
13+
rm -f compiled.js
14+
15+

0 commit comments

Comments
 (0)