From: Robert Varga Date: Fri, 4 Sep 2015 14:49:58 +0000 (+0200) Subject: Introduce SharedSingletonMap X-Git-Tag: release/beryllium~332 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=94ce73de754dde1d931d7d19e6eeb79de997bdef;p=yangtools.git Introduce SharedSingletonMap ImmutableMap can be very expensive in terms of memory overhead when storing a large number of singleton maps which have the same keySet(), because it instantiates an ImmutableSet, which remains cached. That ends up costing 24 bytes in local testing. This patch implements a replacement class which retains the same properties, but shares the keySet instances -- thus lowering the overall overhead. For these instances we introduce an SingletonSet class, which is memory-efficient and gives us direct access to the container entry. This leaves entrySet() as the only method which allocates objects. We do not cache returned objects in the expectation any caller users them as pure DTOs, hence they end up being cheap. Change-Id: I64e74d8e661d2689d32bea31e1cdf72e87ed64af Signed-off-by: Robert Varga --- diff --git a/common/util/src/main/java/org/opendaylight/yangtools/util/ImmutableOffsetMap.java b/common/util/src/main/java/org/opendaylight/yangtools/util/ImmutableOffsetMap.java index b9c374fbdf..bbab15649c 100644 --- a/common/util/src/main/java/org/opendaylight/yangtools/util/ImmutableOffsetMap.java +++ b/common/util/src/main/java/org/opendaylight/yangtools/util/ImmutableOffsetMap.java @@ -80,23 +80,25 @@ public class ImmutableOffsetMap extends AbstractLazyValueMap impleme * @return An isolated, immutable copy of the input map */ @Nonnull public static Map copyOf(@Nonnull final Map m) { - // Prevent a copy - if (m instanceof ImmutableOffsetMap) { + // Prevent a copy. Note that ImmutableMap is not listed here because of its potentially larger keySet overhead. + if (m instanceof ImmutableOffsetMap || m instanceof SharedSingletonMap) { return m; } - // Better-packed + // Familiar and efficient to copy + if (m instanceof MutableOffsetMap) { + return ((MutableOffsetMap) m).toUnmodifiableMap(); + } + final int size = m.size(); if (size == 0) { + // Shares a single object return ImmutableMap.of(); } if (size == 1) { - return ImmutableMap.copyOf(m); - } - - // Familiar and efficient - if (m instanceof MutableOffsetMap) { - return ((MutableOffsetMap) m).toUnmodifiableMap(); + // Efficient single-entry implementation + final Entry e = m.entrySet().iterator().next(); + return SharedSingletonMap.of(e.getKey(), e.getValue()); } final Map offsets = OffsetMapCache.offsetsFor(m.keySet()); diff --git a/common/util/src/main/java/org/opendaylight/yangtools/util/MutableOffsetMap.java b/common/util/src/main/java/org/opendaylight/yangtools/util/MutableOffsetMap.java index 67eb8dadde..4be9ed99cc 100644 --- a/common/util/src/main/java/org/opendaylight/yangtools/util/MutableOffsetMap.java +++ b/common/util/src/main/java/org/opendaylight/yangtools/util/MutableOffsetMap.java @@ -51,7 +51,7 @@ public class MutableOffsetMap extends AbstractLazyValueMap implement this(Collections.emptySet()); } - public MutableOffsetMap(final Collection keySet) { + protected MutableOffsetMap(final Collection keySet) { if (!keySet.isEmpty()) { removed = keySet.size(); offsets = OffsetMapCache.offsetsFor(keySet); @@ -66,9 +66,11 @@ public class MutableOffsetMap extends AbstractLazyValueMap implement } protected MutableOffsetMap(final ImmutableOffsetMap m) { - this.offsets = m.offsets(); - this.objects = m.objects(); - this.newKeys = new LinkedHashMap<>(); + this(m.offsets(), m.objects()); + } + + protected MutableOffsetMap(final Map m) { + this(OffsetMapCache.offsetsFor(m.keySet()), m.values().toArray()); } protected MutableOffsetMap(final MutableOffsetMap m) { @@ -78,6 +80,34 @@ public class MutableOffsetMap extends AbstractLazyValueMap implement this.removed = m.removed; } + private MutableOffsetMap(final Map offsets, final Object[] objects) { + this.offsets = Preconditions.checkNotNull(offsets); + this.objects = Preconditions.checkNotNull(objects); + this.newKeys = new LinkedHashMap<>(); + } + + public static MutableOffsetMap copyOf(final Map m) { + if (m instanceof MutableOffsetMap) { + return ((MutableOffsetMap) m).clone(); + } + if (m instanceof ImmutableOffsetMap) { + return ((ImmutableOffsetMap) m).toModifiableMap(); + } + + return new MutableOffsetMap<>(m); + } + + public static MutableOffsetMap forOffsets(final Map offsets) { + final Object[] objects = new Object[offsets.size()]; + Arrays.fill(objects, NO_VALUE); + + return new MutableOffsetMap<>(offsets, objects); + } + + public static MutableOffsetMap forKeySet(final Collection keySet) { + return forOffsets(OffsetMapCache.offsetsFor(keySet)); + } + @Override public final int size() { return offsets.size() + newKeys.size() - removed; @@ -249,7 +279,7 @@ public class MutableOffsetMap extends AbstractLazyValueMap implement } @Override - public MutableOffsetMap clone() throws CloneNotSupportedException { + public MutableOffsetMap clone() { return new MutableOffsetMap(this); } diff --git a/common/util/src/main/java/org/opendaylight/yangtools/util/SharedSingletonMap.java b/common/util/src/main/java/org/opendaylight/yangtools/util/SharedSingletonMap.java new file mode 100644 index 0000000000..17ff0d985a --- /dev/null +++ b/common/util/src/main/java/org/opendaylight/yangtools/util/SharedSingletonMap.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.yangtools.util; + +import com.google.common.annotations.Beta; +import com.google.common.base.Preconditions; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import java.io.Serializable; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Map; + +/** + * Implementation of the {@link Map} interface which stores a single mapping. The key set is shared among all instances + * which contain the same key. This implementation does not support null keys or values. + * + * @param the type of keys maintained by this map + * @param the type of mapped values + */ +@Beta +public final class SharedSingletonMap implements Serializable, UnmodifiableMapPhase { + private static final long serialVersionUID = 1L; + private static final LoadingCache> CACHE = CacheBuilder.newBuilder().weakValues() + .build(new CacheLoader>() { + @Override + public SingletonSet load(final Object key) { + return SingletonSet.of(key); + } + }); + private final SingletonSet keySet; + private final V value; + private int hashCode; + + @SuppressWarnings("unchecked") + private SharedSingletonMap(final K key, final V value) { + this.keySet = (SingletonSet) CACHE.getUnchecked(key); + this.value = Preconditions.checkNotNull(value); + } + + public static SharedSingletonMap of(final K key, final V value) { + return new SharedSingletonMap<>(key, value); + } + + public static SharedSingletonMap copyOf(final Map m) { + Preconditions.checkArgument(m.size() == 1); + + final Entry e = m.entrySet().iterator().next(); + return new SharedSingletonMap<>(e.getKey(), e.getValue()); + } + + @Override + public ModifiableMapPhase toModifiableMap() { + return new MutableOffsetMap(this); + } + + @Override + public SingletonSet> entrySet() { + return SingletonSet.>of(new SimpleImmutableEntry<>(keySet.getElement(), value)); + } + + @Override + public SingletonSet keySet() { + return keySet; + } + + @Override + public SingletonSet values() { + return SingletonSet.of(value); + } + + @Override + public boolean containsKey(final Object key) { + return keySet.contains(key); + } + + @Override + public boolean containsValue(final Object value) { + return this.value.equals(value); + } + + @Override + public V get(final Object key) { + return keySet.contains(key) ? value : null; + } + + @Override + public int size() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public V put(final K key, final V value) { + throw new UnsupportedOperationException(); + } + + @Override + public V remove(final Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(final Map m) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = keySet.getElement().hashCode() ^ value.hashCode(); + } + return hashCode; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Map)) { + return false; + } + + final Map m = (Map)obj; + return m.size() == 1 && value.equals(m.get(keySet.getElement())); + } + + @Override + public String toString() { + return "{" + keySet.getElement() + '=' + value + '}'; + } +} diff --git a/common/util/src/main/java/org/opendaylight/yangtools/util/SingletonSet.java b/common/util/src/main/java/org/opendaylight/yangtools/util/SingletonSet.java new file mode 100644 index 0000000000..9587d1104e --- /dev/null +++ b/common/util/src/main/java/org/opendaylight/yangtools/util/SingletonSet.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.yangtools.util; + +import com.google.common.annotations.Beta; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterators; +import java.io.Serializable; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.concepts.Immutable; + +/** + * A {@link Set} containing a single value. For some reason neither Java nor Guava provide direct access to the retained + * element -- which is desirable in some situations, as is the case in {@link SharedSingletonMap#entrySet()}. + */ +@Beta +public abstract class SingletonSet implements Set, Immutable, Serializable { + private static final long serialVersionUID = 1L; + + private static final SingletonSet NULL_SINGLETON = new SingletonSet() { + private static final long serialVersionUID = 1L; + + @Override + public boolean contains(final Object o) { + return o == null; + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public Object getElement() { + return null; + } + + @Override + public String toString() { + return "[null]"; + } + + private Object readResolve() { + return NULL_SINGLETON; + } + }; + + @SuppressWarnings("unchecked") + public static SingletonSet of(@Nonnull final E element) { + if (element == null) { + return (SingletonSet) NULL_SINGLETON; + } + return new RegularSingletonSet(element); + } + + public abstract E getElement(); + + @Override + public final int size() { + return 1; + } + + @Override + public final boolean isEmpty() { + return false; + } + + @Override + public final Iterator iterator() { + return Iterators.singletonIterator(getElement()); + } + + @Override + public final Object[] toArray() { + return new Object[] { getElement() }; + } + + @SuppressWarnings("unchecked") + @Override + public final T[] toArray(final T[] a) { + if (a.length > 0) { + a[0] = (T)getElement(); + return a; + } + + return (T[]) new Object[] { (T) getElement() }; + } + + @Override + public final boolean add(final E e) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean remove(final Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean containsAll(final Collection c) { + if (c.isEmpty()) { + return true; + } + if (c.size() != 1) { + return false; + } + + return otherContains(c); + } + + @Override + public final boolean addAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean retainAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean removeAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public final void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public abstract int hashCode(); + + @Override + public final boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Set)) { + return false; + } + + final Set s = (Set)obj; + return s.size() == 1 && otherContains(s); + } + + private boolean otherContains(final Collection other) { + try { + return other.contains(getElement()); + } catch (ClassCastException | NullPointerException e) { + return false; + } + } + + private static final class RegularSingletonSet extends SingletonSet { + private static final long serialVersionUID = 1L; + private final E element; + + RegularSingletonSet(final E element) { + this.element = Preconditions.checkNotNull(element); + } + + @Override + public boolean contains(final Object o) { + return element.equals(o); + } + + @Override + public E getElement() { + return element; + } + + @Override + public int hashCode() { + return getElement().hashCode(); + } + + @Override + public String toString() { + return "[" + element + ']'; + } + } +} diff --git a/common/util/src/test/java/org/opendaylight/yangtools/util/OffsetMapTest.java b/common/util/src/test/java/org/opendaylight/yangtools/util/OffsetMapTest.java index 9022228ad4..5aef8d12c3 100644 --- a/common/util/src/test/java/org/opendaylight/yangtools/util/OffsetMapTest.java +++ b/common/util/src/test/java/org/opendaylight/yangtools/util/OffsetMapTest.java @@ -60,7 +60,7 @@ public class OffsetMapTest { final Map result = ImmutableOffsetMap.copyOf(source); assertEquals(source, result); - assertTrue(result instanceof ImmutableMap); + assertTrue(result instanceof SharedSingletonMap); } @Test diff --git a/common/util/src/test/java/org/opendaylight/yangtools/util/SharedSingletonMapTest.java b/common/util/src/test/java/org/opendaylight/yangtools/util/SharedSingletonMapTest.java new file mode 100644 index 0000000000..3e0bb753df --- /dev/null +++ b/common/util/src/test/java/org/opendaylight/yangtools/util/SharedSingletonMapTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.yangtools.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import org.junit.Test; + +public class SharedSingletonMapTest { + private static UnmodifiableMapPhase create() { + return SharedSingletonMap.of("k1", "v1"); + } + + @Test + public void testSimpleOperations() { + final Map m = create(); + + assertFalse(m.isEmpty()); + assertEquals(1, m.size()); + + assertTrue(m.containsKey("k1")); + assertFalse(m.containsKey(null)); + assertFalse(m.containsKey("v1")); + + assertTrue(m.containsValue("v1")); + assertFalse(m.containsValue(null)); + assertFalse(m.containsValue("k1")); + + assertEquals("v1", m.get("k1")); + assertNull(m.get(null)); + assertNull(m.get("v1")); + + assertFalse(m.equals(null)); + assertTrue(m.equals(m)); + assertFalse(m.equals("")); + + final Map same = Collections.singletonMap("k1", "v1"); + assertEquals(same.toString(), m.toString()); + assertTrue(same.equals(m)); + assertTrue(m.equals(same)); + assertEquals(same.entrySet(), m.entrySet()); + assertEquals(same.values(), m.values()); + + // Perform twice to exercise the cache + assertEquals(same.hashCode(), m.hashCode()); + assertEquals(same.hashCode(), m.hashCode()); + + assertFalse(m.equals(Collections.singletonMap(null, null))); + assertFalse(m.equals(Collections.singletonMap("k1", null))); + assertFalse(m.equals(Collections.singletonMap(null, "v1"))); + assertFalse(m.equals(Collections.singletonMap("k1", "v2"))); + + final Set set = m.keySet(); + assertTrue(set instanceof SingletonSet); + assertTrue(set.contains("k1")); + } + + @Test(expected=UnsupportedOperationException.class) + public void testClear() { + create().clear(); + } + + @Test(expected=UnsupportedOperationException.class) + public void testPut() { + create().put(null, null); + } + + @Test(expected=UnsupportedOperationException.class) + public void testPutAll() { + create().putAll(Collections.singletonMap("", "")); + } + + @Test(expected=UnsupportedOperationException.class) + public void testRemove() { + create().remove(null); + } +} diff --git a/common/util/src/test/java/org/opendaylight/yangtools/util/SingletonSetTest.java b/common/util/src/test/java/org/opendaylight/yangtools/util/SingletonSetTest.java new file mode 100644 index 0000000000..9cd7f35332 --- /dev/null +++ b/common/util/src/test/java/org/opendaylight/yangtools/util/SingletonSetTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.yangtools.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import java.util.Collections; +import java.util.Iterator; +import org.junit.Test; + +public class SingletonSetTest { + private static final String ELEMENT = "element"; + + private static SingletonSet nullSet() { + return SingletonSet.of(null); + } + + @Test + public void testNullSingleton() { + final SingletonSet s = nullSet(); + + assertFalse(s.isEmpty()); + assertEquals(1, s.size()); + assertFalse(s.contains("")); + assertTrue(s.contains(null)); + assertNull(s.getElement()); + assertEquals(0, s.hashCode()); + assertTrue(s.equals(Collections.singleton(null))); + assertFalse(s.equals(Collections.singleton(""))); + assertFalse(s.equals("")); + assertTrue(s.equals(s)); + assertFalse(s.equals(null)); + assertEquals(Collections.singleton(null).toString(), s.toString()); + } + + @Test + public void testRegularSingleton() { + final SingletonSet s = SingletonSet.of(ELEMENT); + + assertFalse(s.isEmpty()); + assertEquals(1, s.size()); + assertFalse(s.contains("")); + assertFalse(s.contains(null)); + assertTrue(s.contains(ELEMENT)); + + assertSame(ELEMENT, s.getElement()); + assertEquals(ELEMENT.hashCode(), s.hashCode()); + assertTrue(s.equals(Collections.singleton(ELEMENT))); + assertFalse(s.equals(Collections.singleton(""))); + assertFalse(s.equals(Collections.singleton(null))); + assertFalse(s.equals("")); + assertTrue(s.equals(s)); + assertFalse(s.equals(null)); + assertEquals(Collections.singleton(ELEMENT).toString(), s.toString()); + } + + @Test + public void testIterator() { + final SingletonSet s = SingletonSet.of(ELEMENT); + final Iterator it = s.iterator(); + + assertTrue(it.hasNext()); + assertSame(ELEMENT, it.next()); + assertFalse(it.hasNext()); + } + + @Test(expected=UnsupportedOperationException.class) + public void testRejectedAdd() { + final SingletonSet s = nullSet(); + s.add(null); + } + + @Test(expected=UnsupportedOperationException.class) + public void testRejectedAddAll() { + final SingletonSet s = nullSet(); + s.addAll(null); + } + + @Test(expected=UnsupportedOperationException.class) + public void testRejectedClear() { + final SingletonSet s = nullSet(); + s.clear(); + } + + @Test(expected=UnsupportedOperationException.class) + public void testRejectedRemove() { + final SingletonSet s = nullSet(); + s.remove(null); + } + + @Test(expected=UnsupportedOperationException.class) + public void testRejectedRemoveAll() { + final SingletonSet s = nullSet(); + s.removeAll(null); + } + + @Test(expected=UnsupportedOperationException.class) + public void testRejectedRetainAll() { + final SingletonSet s = nullSet(); + s.retainAll(null); + } +} diff --git a/yang/yang-data-api/src/main/java/org/opendaylight/yangtools/yang/data/api/YangInstanceIdentifier.java b/yang/yang-data-api/src/main/java/org/opendaylight/yangtools/yang/data/api/YangInstanceIdentifier.java index 81f474db58..849f7f0843 100644 --- a/yang/yang-data-api/src/main/java/org/opendaylight/yangtools/yang/data/api/YangInstanceIdentifier.java +++ b/yang/yang-data-api/src/main/java/org/opendaylight/yangtools/yang/data/api/YangInstanceIdentifier.java @@ -13,7 +13,6 @@ import com.google.common.base.Preconditions; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import java.io.Serializable; @@ -34,6 +33,7 @@ import org.opendaylight.yangtools.concepts.Immutable; import org.opendaylight.yangtools.concepts.Path; import org.opendaylight.yangtools.util.HashCodeBuilder; import org.opendaylight.yangtools.util.ImmutableOffsetMap; +import org.opendaylight.yangtools.util.SharedSingletonMap; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.common.QNameModule; import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode; @@ -497,12 +497,12 @@ public abstract class YangInstanceIdentifier implements Path keyValues) { super(node); - // Retains ImmutableMap for maps with size() <= 1. For larger sizes uses a shared key set. + // Retains ImmutableMap for empty maps. For larger sizes uses a shared key set. this.keyValues = ImmutableOffsetMap.copyOf(keyValues); } public NodeIdentifierWithPredicates(final QName node, final QName key, final Object value) { - this(node, ImmutableMap.of(key, value)); + this(node, SharedSingletonMap.of(key, value)); } public Map getKeyValues() {