Skip to content

Commit

Permalink
make sure we only use optimized set operations when both collection s…
Browse files Browse the repository at this point in the history
…hare equality semantics
  • Loading branch information
ztellman committed Oct 22, 2018
1 parent 20be367 commit 29cb1ef
Show file tree
Hide file tree
Showing 21 changed files with 404 additions and 147 deletions.
2 changes: 1 addition & 1 deletion doc/comparison.md
Expand Up @@ -76,7 +76,7 @@ The two mutable collections are significantly faster, while for smaller collecti

![](../benchmarks/images/map_iterate.png)

Unlike Java's `HashMap` and `HashSet`, Bifurcan's `LinearMap` and `LinearSet` store their entries contiguously, which means that they can be cloned using `System.arraycopy()`. This makes iteration over these data structures significantly faster.
Unlike Java's `HashMap` and `HashSet`, Bifurcan's `LinearMap` and `LinearSet` store their entries contiguously, which makes both iteration and cloning significantly faster.

Bifurcan, Capsule, and Scala are all comparable to Java's `HashMap`, while the others are constant factor slower.

Expand Down
8 changes: 7 additions & 1 deletion project.clj
Expand Up @@ -37,7 +37,13 @@
:jvm-opts ^:replace ["-server"
"-XX:+UseG1GC"
"-XX:-OmitStackTraceInFastThrow"
"-ea:io.lacuna..."]
"-ea:io.lacuna..."

#_"-XX:+UnlockDiagnosticVMOptions"
#_"-XX:+PrintAssembly"
#_"-XX:CompileCommand=print,io.lacuna.bifurcan.nodes.Util::mergeState"
#_"-XX:CompileCommand=dontinline,io.lacuna.bifurcan.nodes.Util::mergeState"
]

:repositories {"usethesource" "http://nexus.usethesource.io/content/repositories/public/"}

Expand Down
15 changes: 11 additions & 4 deletions src/io/lacuna/bifurcan/DirectedAcyclicGraph.java
Expand Up @@ -33,6 +33,10 @@ public DirectedAcyclicGraph() {
this(new DirectedGraph<>(), new Set<>(), new Set<>());
}

public DirectedAcyclicGraph(ToIntFunction<V> hashFn, BiPredicate<V, V> equalsFn) {
this(new DirectedGraph<>(hashFn, equalsFn), new Set<>(hashFn, equalsFn), new Set<>(hashFn, equalsFn));
}

/**
* @return a directed acyclic graph equivalent to {@code graph}
* @throws CycleException if {@code graph} contains a cycle
Expand All @@ -42,10 +46,13 @@ public static <V, E> DirectedAcyclicGraph<V, E> from(DirectedGraph<V, E> graph)
throw new CycleException();
}

return new DirectedAcyclicGraph<>(
graph,
graph.vertices().stream().filter(v -> graph.in(v).size() == 0).collect(Sets.collector()),
graph.vertices().stream().filter(v -> graph.out(v).size() == 0).collect(Sets.collector()));
Set<V> top = new Set<>(graph.vertexHash(), graph.vertexEquality()).linear();
Set<V> bottom = new Set<>(graph.vertexHash(), graph.vertexEquality()).linear();

graph.vertices().stream().filter(v -> graph.in(v).size() == 0).forEach(top::add);
graph.vertices().stream().filter(v -> graph.out(v).size() == 0).forEach(bottom::add);

return new DirectedAcyclicGraph<>(graph, top.forked(), bottom.forked());
}

public Set<V> top() {
Expand Down
2 changes: 1 addition & 1 deletion src/io/lacuna/bifurcan/FloatMap.java
Expand Up @@ -20,7 +20,7 @@ public class FloatMap<V> implements ISortedMap<Double, V>, Cloneable {

private static final ToIntFunction<Double> HASH = n -> IntMap.HASH.applyAsInt(Encodings.doubleToLong(n));

private IntMap<V> map;
public IntMap<V> map;

public static <V> FloatMap<V> from(IMap<Number, V> m) {
if (m instanceof FloatMap) {
Expand Down
2 changes: 1 addition & 1 deletion src/io/lacuna/bifurcan/Graph.java
Expand Up @@ -39,7 +39,7 @@ boolean equals(BiPredicate<V, V> equalsFn, VertexSet<V> t) {
private Map<VertexSet<V>, E> edges;

public Graph() {
this(Objects::hash, Objects::equals);
this(Maps.DEFAULT_HASH_CODE, Maps.DEFAULT_EQUALS);
}

public Graph(ToIntFunction<V> hashFn, BiPredicate<V, V> equalsFn) {
Expand Down
8 changes: 2 additions & 6 deletions src/io/lacuna/bifurcan/IMap.java
Expand Up @@ -22,16 +22,12 @@ public interface IMap<K, V> extends
/**
* @return the hash function used by the map
*/
default ToIntFunction<K> keyHash() {
return Objects::hashCode;
}
ToIntFunction<K> keyHash();

/**
* @return the key equality semantics used by the map
*/
default BiPredicate<K, K> keyEquality() {
return Objects::equals;
}
BiPredicate<K, K> keyEquality();

/**
* @return the value under {@code key}, or {@code defaultValue} if there is no such key
Expand Down
8 changes: 2 additions & 6 deletions src/io/lacuna/bifurcan/ISet.java
Expand Up @@ -23,16 +23,12 @@ public interface ISet<V> extends
/**
* @return the hash function used by the set
*/
default ToIntFunction<V> valueHash() {
return Objects::hashCode;
}
ToIntFunction<V> valueHash();

/**
* @return the equality semantics used by the set
*/
default BiPredicate<V, V> valueEquality() {
return Objects::equals;
}
BiPredicate<V, V> valueEquality();

/**
* @return true, if the set contains {@code value}
Expand Down
12 changes: 11 additions & 1 deletion src/io/lacuna/bifurcan/IntMap.java
Expand Up @@ -27,6 +27,7 @@ public class IntMap<V> implements ISortedMap<Long, V>, Cloneable {

final Object editor;
private Node<V> neg, pos;
private int hash = -1;

/**
* @param m another map
Expand Down Expand Up @@ -183,6 +184,7 @@ public IntMap<V> put(long key, V value, BinaryOperator<V> merge, Object editor)
if (neg == negPrime) {
return this;
} else if (isLinear()) {
hash = -1;
neg = negPrime;
return this;
} else {
Expand All @@ -193,6 +195,7 @@ public IntMap<V> put(long key, V value, BinaryOperator<V> merge, Object editor)
if (pos == posPrime) {
return this;
} else if (isLinear()) {
hash = -1;
pos = posPrime;
return this;
} else {
Expand Down Expand Up @@ -224,6 +227,7 @@ public IntMap<V> remove(long key, Object editor) {
if (neg == negPrime) {
return this;
} else if (isLinear()) {
hash = -1;
neg = negPrime;
return this;
} else {
Expand All @@ -234,6 +238,7 @@ public IntMap<V> remove(long key, Object editor) {
if (pos == posPrime) {
return this;
} else if (isLinear()) {
hash = -1;
pos = posPrime;
return this;
} else {
Expand Down Expand Up @@ -402,9 +407,14 @@ public List<IntMap<V>> split(int parts) {
return result.forked();
}



@Override
public int hashCode() {
return (int) Maps.hash(this);
if (hash == -1) {
hash = (int) Maps.hash(this);
}
return hash;
}

@Override
Expand Down
27 changes: 18 additions & 9 deletions src/io/lacuna/bifurcan/LinearMap.java
Expand Up @@ -102,7 +102,7 @@ public LinearMap() {
* @param initialCapacity the initial capacity of the map
*/
public LinearMap(int initialCapacity) {
this(initialCapacity, Objects::hashCode, Objects::equals);
this(initialCapacity, Maps.DEFAULT_HASH_CODE, Maps.DEFAULT_EQUALS);
}

/**
Expand Down Expand Up @@ -368,14 +368,14 @@ public LinearMap<K, V> union(IMap<K, V> m) {
}

@Override
public LinearMap<K, V> merge(IMap<K, V> o, BinaryOperator<V> mergeFn) {
if (o.size() == 0) {
public LinearMap<K, V> merge(IMap<K, V> m, BinaryOperator<V> mergeFn) {
if (m.size() == 0) {
return this.clone();
} else if (o instanceof LinearMap) {
return merge((LinearMap<K, V>) o, mergeFn);
} else if (m instanceof LinearMap && Maps.equivEquality(this, m)) {
return merge((LinearMap<K, V>) m, mergeFn);
} else {
LinearMap<K, V> result = this.clone();
for (IEntry<K, V> e : o.entries()) {
for (IEntry<K, V> e : m.entries()) {
result.put(e.key(), e.value(), mergeFn);
}
return result;
Expand All @@ -384,16 +384,25 @@ public LinearMap<K, V> merge(IMap<K, V> o, BinaryOperator<V> mergeFn) {

@Override
public LinearMap<K, V> difference(IMap<K, ?> m) {
if (m instanceof LinearMap) {
if (m instanceof LinearMap && Maps.equivEquality(this, m)) {
return difference((LinearMap<K, ?>) m);
} else {
return (LinearMap<K, V>) Maps.difference(this.clone(), m.keys());
}
}

@Override
public IMap<K, V> difference(ISet<K> keys) {
if (keys instanceof LinearSet && Maps.equivEquality(this, keys)) {
return difference(((LinearSet<K>) keys).map);
} else {
return Maps.difference(this.clone(), keys);
}
}

@Override
public LinearMap<K, V> intersection(IMap<K, ?> m) {
if (m instanceof LinearMap) {
if (m instanceof LinearMap && Maps.equivEquality(this, m)) {
return intersection((LinearMap<K, ?>) m);
} else {
return (LinearMap<K, V>) Maps.intersection(new LinearMap<>(), this, m.keys());
Expand All @@ -402,7 +411,7 @@ public LinearMap<K, V> intersection(IMap<K, ?> m) {

@Override
public LinearMap<K, V> intersection(ISet<K> keys) {
if (keys instanceof LinearSet) {
if (keys instanceof LinearSet && Maps.equivEquality(this, keys)) {
return intersection(((LinearSet<K>) keys).map);
} else {
return (LinearMap<K, V>) Maps.intersection(new LinearMap<>(), this, keys);
Expand Down
2 changes: 1 addition & 1 deletion src/io/lacuna/bifurcan/LinearSet.java
Expand Up @@ -83,7 +83,7 @@ public LinearSet() {
* @param initialCapacity the initial capacity of the set
*/
public LinearSet(int initialCapacity) {
this(initialCapacity, Objects::hashCode, Objects::equals);
this(initialCapacity, Maps.DEFAULT_HASH_CODE, Maps.DEFAULT_EQUALS);
}

/**
Expand Down
32 changes: 12 additions & 20 deletions src/io/lacuna/bifurcan/Map.java
Expand Up @@ -27,7 +27,7 @@ public class Map<K, V> implements IMap<K, V>, Cloneable {

private final BiPredicate<K, K> equalsFn;
private final ToIntFunction<K> hashFn;
public Node<K, V> root;
private Node<K, V> root;
private int hash = -1;
final Object editor;

Expand All @@ -52,7 +52,7 @@ public static <K, V> Map<K, V> from(IMap<K, V> map) {
* @return a forked map with the same entries
*/
public static <K, V> Map<K, V> from(java.util.Map<K, V> map) {
return from(map.entrySet());
return map.entrySet().stream().collect(Maps.collector(java.util.Map.Entry::getKey, java.util.Map.Entry::getValue));
}

/**
Expand Down Expand Up @@ -81,14 +81,6 @@ public static <K, V> Map<K, V> from(IList<IEntry<K, V>> entries) {
return entries.stream().collect(Maps.collector(IEntry::key, IEntry::value));
}

/**
* @param entries a collection of {@code java.util.Map.Entry} objects
* @return a forked map containing these entries
*/
public static <K, V> Map<K, V> from(Collection<java.util.Map.Entry<K, V>> entries) {
return entries.stream().collect(Maps.collector(java.util.Map.Entry::getKey, java.util.Map.Entry::getValue));
}

/**
* Creates a map.
*
Expand All @@ -100,7 +92,7 @@ public Map(ToIntFunction<K> hashFn, BiPredicate<K, K> equalsFn) {
}

public Map() {
this(Node.EMPTY, Objects::hashCode, Objects::equals, false);
this(Node.EMPTY, Maps.DEFAULT_HASH_CODE, Maps.DEFAULT_EQUALS, false);
}

private Map(Node<K, V> root, ToIntFunction<K> hashFn, BiPredicate<K, K> equalsFn, boolean linear) {
Expand Down Expand Up @@ -238,18 +230,18 @@ public Map<K, V> union(IMap<K, V> m) {
}

@Override
public Map<K, V> merge(IMap<K, V> b, BinaryOperator<V> mergeFn) {
if (b instanceof Map) {
Node<K, V> rootPrime = MapNodes.merge(0, editor, root, ((Map) b).root, equalsFn, mergeFn);
public Map<K, V> merge(IMap<K, V> m, BinaryOperator<V> mergeFn) {
if (m instanceof Map && Maps.equivEquality(this, m)) {
Node<K, V> rootPrime = MapNodes.merge(0, editor, root, ((Map) m).root, equalsFn, mergeFn);
return new Map<>(rootPrime, hashFn, equalsFn, isLinear());
} else {
return (Map<K, V>) Maps.merge(this.clone(), b, mergeFn);
return (Map<K, V>) Maps.merge(this.clone(), m, mergeFn);
}
}

@Override
public Map<K, V> difference(ISet<K> keys) {
if (keys instanceof Set) {
if (keys instanceof Set && Maps.equivEquality(this, keys)) {
return difference(((Set<K>) keys).map);
} else {
return (Map<K, V>) Maps.difference(this.clone(), keys);
Expand All @@ -258,7 +250,7 @@ public Map<K, V> difference(ISet<K> keys) {

@Override
public Map<K, V> intersection(ISet<K> keys) {
if (keys instanceof Set) {
if (keys instanceof Set && Maps.equivEquality(this, keys)) {
return intersection(((Set<K>) keys).map);
} else {
Map<K, V> map = (Map<K, V>) Maps.intersection(new Map<K, V>().linear(), this, keys);
Expand All @@ -268,7 +260,7 @@ public Map<K, V> intersection(ISet<K> keys) {

@Override
public Map<K, V> difference(IMap<K, ?> m) {
if (m instanceof Map) {
if (m instanceof Map && Maps.equivEquality(this, m)) {
Node<K, V> rootPrime = MapNodes.difference(0, editor, root, ((Map) m).root, equalsFn);
return new Map<>(rootPrime == null ? Node.EMPTY : rootPrime, hashFn, equalsFn, isLinear());
} else {
Expand All @@ -278,7 +270,7 @@ public Map<K, V> difference(IMap<K, ?> m) {

@Override
public Map<K, V> intersection(IMap<K, ?> m) {
if (m instanceof Map) {
if (m instanceof Map && Maps.equivEquality(this, m)) {
Node<K, V> rootPrime = MapNodes.intersection(0, editor, root, ((Map) m).root, equalsFn);
return new Map<>(rootPrime == null ? Node.EMPTY : rootPrime, hashFn, equalsFn, isLinear());
} else {
Expand Down Expand Up @@ -311,7 +303,7 @@ public int hashCode() {

@Override
public boolean equals(IMap<K, V> m, BiPredicate<V, V> valEquals) {
if (m instanceof Map) {
if (m instanceof Map && keyHash() == m.keyHash()) {
return root.equals(((Map<K, V>) m).root, equalsFn, valEquals);
} else {
return Maps.equals(this, m, valEquals);
Expand Down

0 comments on commit 29cb1ef

Please sign in to comment.