You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ag...@apache.org on 2019/01/20 10:29:51 UTC

[ignite] branch master updated: IGNITE-10877 Reduce memory footprint and allocation pressure of affinity assignments - Fixes #5796

This is an automated email from the ASF dual-hosted git repository.

agoncharuk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 22c0c7e  IGNITE-10877 Reduce memory footprint and allocation pressure of affinity assignments - Fixes #5796
22c0c7e is described below

commit 22c0c7ebad5265b66e0060d25cce9ce4568234a8
Author: Pavel Voronkin <pv...@gridgain.com>
AuthorDate: Sun Jan 20 13:23:56 2019 +0300

    IGNITE-10877 Reduce memory footprint and allocation pressure of affinity assignments - Fixes #5796
---
 modules/benchmarks/pom.xml                         |   6 +
 .../SmallHashSetsVsReadOnlyViewBenchmark.java      | 154 ++++++++++
 .../jol/GridAffinityAssignmentJolBenchmark.java    | 177 +++++++++++
 .../ignite/internal/benchmarks/model/Node.java     |  44 +++
 .../org/apache/ignite/IgniteSystemProperties.java  |  15 +
 .../processors/affinity/AffinityAssignment.java    |  34 ++-
 .../affinity/GridAffinityAssignment.java           | 133 ++++----
 .../processors/affinity/GridAffinityUtils.java     |   2 +-
 .../affinity/HistoryAffinityAssignment.java        |  45 +--
 .../cache/CacheAffinitySharedManager.java          |   2 +-
 .../dht/topology/GridDhtPartitionTopologyImpl.java |   7 +-
 .../apache/ignite/internal/util/BitSetIntSet.java  | 184 +++++++++++
 .../affinity/GridAffinityAssignmentTest.java       | 217 +++++++++++++
 .../GridAffinityAssignmentTestNoOptimizations.java |  41 +++
 .../ignite/internal/util/BitSetIntSetTest.java     | 339 +++++++++++++++++++++
 .../ignite/testsuites/IgniteBasicTestSuite.java    |   6 +
 16 files changed, 1316 insertions(+), 90 deletions(-)

diff --git a/modules/benchmarks/pom.xml b/modules/benchmarks/pom.xml
index e5a8216..129387a 100644
--- a/modules/benchmarks/pom.xml
+++ b/modules/benchmarks/pom.xml
@@ -52,6 +52,12 @@
         </dependency>
 
         <dependency>
+            <groupId>org.openjdk.jol</groupId>
+            <artifactId>jol-core</artifactId>
+            <version>0.9</version>
+        </dependency>
+
+        <dependency>
             <groupId>org.openjdk.jmh</groupId>
             <artifactId>jmh-core</artifactId>
             <version>${jmh.version}</version>
diff --git a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/collections/SmallHashSetsVsReadOnlyViewBenchmark.java b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/collections/SmallHashSetsVsReadOnlyViewBenchmark.java
new file mode 100644
index 0000000..d130792
--- /dev/null
+++ b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/collections/SmallHashSetsVsReadOnlyViewBenchmark.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.benchmarks.jmh.collections;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+import org.apache.ignite.internal.benchmarks.jmh.JmhAbstractBenchmark;
+import org.apache.ignite.internal.benchmarks.jmh.runner.JmhIdeBenchmarkRunner;
+import org.apache.ignite.internal.benchmarks.model.Node;
+import org.apache.ignite.internal.processors.affinity.AffinityAssignment;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.lang.IgniteClosure;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static org.openjdk.jmh.annotations.Mode.Throughput;
+
+/**
+ * Comparison of HashMap vs view on List on small sizes.
+ */
+@State(Scope.Benchmark)
+@OutputTimeUnit(NANOSECONDS)
+@BenchmarkMode(Throughput)
+public class SmallHashSetsVsReadOnlyViewBenchmark extends JmhAbstractBenchmark {
+    /** */
+    private static final int SIZE = AffinityAssignment.IGNITE_AFFINITY_BACKUPS_THRESHOLD;
+
+    /** */
+    private static final int PARTS = 8192;
+
+    /**
+     *
+     * @param args Args.
+     * @throws Exception Exception.
+     */
+    public static void main(String[] args) throws Exception {
+        JmhIdeBenchmarkRunner.create()
+            .threads(1)
+            .measurementIterations(20)
+            .benchmarks(SmallHashSetsVsReadOnlyViewBenchmark.class.getSimpleName())
+            .run();
+    }
+
+    /** */
+    private final Random random = new Random();
+
+    /** */
+    private final List<Collection<UUID>> hashSets = new ArrayList<>();
+
+    /** */
+    private final List<List<Node>> lists = new ArrayList<>();
+
+    /** */
+    private final Node[] nodes = new Node[SIZE];
+
+    /** */
+    @Setup
+    public void setup() {
+        for (int i = 0; i < SIZE; i++)
+            nodes[i] = new Node(UUID.randomUUID());
+
+        for (int i= 0; i < PARTS; i++) {
+            Collection<UUID> hashSet = new HashSet<>();
+
+            for (int j = 0; j < SIZE; j++)
+                hashSet.add(nodes[j].getUuid());
+
+            hashSets.add(hashSet);
+
+            List<Node> list = new ArrayList<>(SIZE);
+
+            for (int j = 0; j < SIZE; j++)
+                list.add(nodes[j]);
+
+            lists.add(list);
+        }
+    }
+
+    /** */
+    @Benchmark
+    public boolean hashSetContainsRandom() {
+        return hashSets.get(random.nextInt(PARTS))
+            .contains(nodes[random.nextInt(SIZE)].getUuid());
+    }
+
+    /** */
+    @Benchmark
+    public boolean readOnlyViewContainsRandom() {
+        return F.viewReadOnly(
+            lists.get(random.nextInt(PARTS)),
+            (IgniteClosure<Node, UUID>)Node::getUuid
+        ).contains(nodes[random.nextInt(SIZE)].getUuid());
+    }
+
+    /** */
+    @Benchmark
+    public boolean hashSetIteratorRandom() {
+        UUID randomUuid = nodes[random.nextInt(SIZE)].getUuid();
+
+        Collection<UUID> col = hashSets.get(random.nextInt(PARTS));
+
+        boolean contains = false;
+
+        for(UUID uuid : col)
+            if (randomUuid.equals(uuid))
+                contains = true;
+
+        return contains;
+    }
+
+    /** */
+    @Benchmark
+    public boolean readOnlyViewIteratorRandom() {
+        UUID randomUuid = nodes[random.nextInt(SIZE)].getUuid();
+
+        Collection<UUID> col = F.viewReadOnly(
+            lists.get(random.nextInt(PARTS)),
+            (IgniteClosure<Node, UUID>)Node::getUuid
+        );
+
+        boolean contains = false;
+
+        for(UUID uuid : col)
+            if (randomUuid.equals(uuid))
+                contains = true;
+
+        return contains;
+    }
+}
+
diff --git a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jol/GridAffinityAssignmentJolBenchmark.java b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jol/GridAffinityAssignmentJolBenchmark.java
new file mode 100644
index 0000000..266e734
--- /dev/null
+++ b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jol/GridAffinityAssignmentJolBenchmark.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.benchmarks.jol;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.apache.ignite.cache.CacheMetrics;
+import org.apache.ignite.cache.affinity.AffinityFunctionContext;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.cluster.ClusterMetrics;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.events.DiscoveryEvent;
+import org.apache.ignite.internal.processors.affinity.AffinityAssignment;
+import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
+import org.apache.ignite.internal.processors.affinity.GridAffinityAssignment;
+import org.apache.ignite.internal.processors.affinity.GridAffinityFunctionContextImpl;
+import org.apache.ignite.lang.IgniteProductVersion;
+import org.apache.ignite.spi.discovery.DiscoveryMetricsProvider;
+import org.apache.ignite.spi.discovery.tcp.internal.TcpDiscoveryNode;
+import org.openjdk.jol.info.GraphLayout;
+
+/**
+ *
+ */
+public class GridAffinityAssignmentJolBenchmark {
+    /**  */
+    private static DiscoveryMetricsProvider metrics = new DiscoveryMetricsProvider() {
+        @Override public ClusterMetrics metrics() {
+            return null;
+        }
+
+        @Override public Map<Integer, CacheMetrics> cacheMetrics() {
+            return null;
+        }
+    };
+
+    /** */
+    private static IgniteProductVersion ver = new IgniteProductVersion();
+
+    /** */
+    private static Field field;
+
+    /** */
+    public static void main(String[] args) throws Exception {
+        RendezvousAffinityFunction aff = new RendezvousAffinityFunction(true, 65000);
+
+        int[] parts = new int[] {1024, 8192, 32768, 65000};
+
+        int[] nodes = new int[] {1, 16, 160, 600};
+
+        // We need to implement compressed bitsets https://issues.apache.org/jira/browse/IGNITE-4554.
+        // On 65k partitions and nodes > 700 HashSet take advantage over BitSet.
+        // After implementation need to check consumption on big clusters.
+        for (int part : parts)
+            for (int node : nodes) {
+                measure(aff, part, node, 0);
+
+                measure(aff, part, node, 3);
+
+                measure(aff, part, node, node);
+            }
+    }
+
+    /**
+     * @param disabled Disabled.
+     */
+    private static void setOptimization(boolean disabled) throws NoSuchFieldException, IllegalAccessException {
+        if (field == null) {
+            field = AffinityAssignment.class.getDeclaredField("IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION");
+
+            Field modifiersField = Field.class.getDeclaredField("modifiers");
+            modifiersField.setAccessible(true);
+            modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
+
+            field.setAccessible(true);
+        }
+
+        field.set(null, disabled);
+    }
+
+    /**
+     * @param aff Aff.
+     * @param parts Parts.
+     * @param nodeCnt Node count.
+     * @param backups Backups.
+     */
+    private static void measure(
+        RendezvousAffinityFunction aff,
+        int parts,
+        int nodeCnt,
+        int backups
+    ) throws Exception {
+        List<ClusterNode> nodes = new ArrayList<>();
+
+        for (int i = 0; i < nodeCnt; i++) {
+            TcpDiscoveryNode node = new TcpDiscoveryNode(
+                UUID.randomUUID(),
+                Collections.singletonList("127.0.0.1"),
+                Collections.singletonList("127.0.0.1"),
+                0,
+                metrics,
+                ver,
+                i
+            );
+            node.setAttributes(new HashMap<>());
+            nodes.add(node);
+        }
+
+        AffinityFunctionContext ctx = new GridAffinityFunctionContextImpl(
+            nodes,
+            new ArrayList<>(),
+            new DiscoveryEvent(),
+            new AffinityTopologyVersion(),
+            backups
+        );
+
+        List<List<ClusterNode>> assignment = aff.assignPartitions(ctx);
+
+        setOptimization(false);
+
+        GridAffinityAssignment ga = new GridAffinityAssignment(
+            new AffinityTopologyVersion(1, 0),
+            assignment,
+            new ArrayList<>()
+        );
+
+        System.gc();
+
+        long totalSize = GraphLayout.parseInstance(ga).totalSize();
+
+        System.out.println("Optimized, parts " + parts
+            +" nodeCount " + nodeCnt
+            +" backups " + backups
+            + " " + totalSize);
+
+        setOptimization(true);
+
+        GridAffinityAssignment ga2 = new GridAffinityAssignment(
+            new AffinityTopologyVersion(1, 0),
+            assignment,
+            new ArrayList<>()
+        );
+
+        System.gc();
+
+        long totalSize2 = GraphLayout.parseInstance(ga2).totalSize();
+
+        System.out.println("Deoptimized, parts " + parts
+            +" nodeCount " + nodeCnt
+            +" backups " + backups
+            + " " + totalSize2);
+
+        if (totalSize > totalSize2)
+            throw new Exception("Optimized AffinityAssignment size " + totalSize +" is more than deoptimized" + totalSize2);
+    }
+}
diff --git a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/model/Node.java b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/model/Node.java
new file mode 100644
index 0000000..fd23ede
--- /dev/null
+++ b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/model/Node.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.benchmarks.model;
+
+import java.util.UUID;
+
+/**
+ *
+ */
+public class Node {
+    /**  */
+    private UUID uuid;
+
+    /**
+     *
+     * @param uuid Uuid.
+     */
+    public Node(UUID uuid) {
+        this.uuid = uuid;
+    }
+
+    /**
+     *
+     * @return UUID.
+     */
+    public UUID getUuid() {
+        return uuid;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java b/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java
index b20d8ad..6b16de5 100644
--- a/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java
+++ b/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java
@@ -1095,6 +1095,21 @@ public final class IgniteSystemProperties {
     public static final String IGNITE_GLOBAL_METASTORAGE_HISTORY_MAX_BYTES = "IGNITE_GLOBAL_METASTORAGE_HISTORY_MAX_BYTES";
 
     /**
+     * Size threshold to allocate and retain additional  HashMap to improve contains()
+     * which leads to extra memory consumption.
+     */
+    public static final String IGNITE_AFFINITY_BACKUPS_THRESHOLD = "IGNITE_AFFINITY_BACKUUPS_THRESHOLD";
+
+    /**
+     * Flag to disable memory optimization:
+     *  BitSets instead of HashSets to store partitions.
+     *  When number of backups per partion is > IGNITE_AFFINITY_BACKUPS_THRESHOLD we use HashMap to improve contains()
+     * which leads to extra memory consumption, otherwise we use view on the
+     * list of cluster nodes to reduce memory consumption on redundant data structures.
+     */
+    public static final String IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION = "IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION";
+
+    /**
      * Enforces singleton.
      */
     private IgniteSystemProperties() {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/AffinityAssignment.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/AffinityAssignment.java
index b603c32..d6f28f6 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/AffinityAssignment.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/AffinityAssignment.java
@@ -17,16 +17,32 @@
 
 package org.apache.ignite.internal.processors.affinity;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.UUID;
+import org.apache.ignite.IgniteSystemProperties;
 import org.apache.ignite.cluster.ClusterNode;
 
 /**
  * Cached affinity calculations.
  */
 public interface AffinityAssignment {
+    /** Size threshold to use Map instead of List view. */
+    int IGNITE_AFFINITY_BACKUPS_THRESHOLD = IgniteSystemProperties.getInteger(
+        IgniteSystemProperties.IGNITE_AFFINITY_BACKUPS_THRESHOLD,
+        5
+    );
+
+    /** Disable memory affinity optimizations. */
+    boolean IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION = IgniteSystemProperties.getBoolean(
+        IgniteSystemProperties.IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION,
+        false
+    );
+
     /**
      * @return Affinity assignment computed by affinity function.
      */
@@ -56,7 +72,7 @@ public interface AffinityAssignment {
      * @param part Partition.
      * @return Affinity nodes IDs.
      */
-    public HashSet<UUID> getIds(int part);
+    public Collection<UUID> getIds(int part);
 
     /**
      * @return Nodes having parimary and backup assignments.
@@ -83,4 +99,18 @@ public interface AffinityAssignment {
      * @return Backup partitions for specified node ID.
      */
     public Set<Integer> backupPartitions(UUID nodeId);
-}
\ No newline at end of file
+
+    /**
+     * Converts List of Cluster Nodes to HashSet of UUIDs wrapped as unmodifiable collection.
+     * @param assignmentPart Source assignment per partition.
+     * @return List of deduplicated collections if ClusterNode's ids.
+     */
+    public default Collection<UUID> assignments2ids(List<ClusterNode> assignmentPart) {
+        Collection<UUID> partIds = new HashSet<>(assignmentPart.size());
+
+        for (ClusterNode node : assignmentPart)
+            partIds.add(node.id());
+
+        return Collections.unmodifiableCollection(partIds);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignment.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignment.java
index 95cf76fc..b820605 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignment.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignment.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.processors.affinity;
 
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -27,6 +28,8 @@ import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.util.BitSetIntSet;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.S;
 
 /**
@@ -50,7 +53,7 @@ public class GridAffinityAssignment implements AffinityAssignment, Serializable
     private final Map<UUID, Set<Integer>> backup;
 
     /** Assignment node IDs */
-    private transient volatile List<HashSet<UUID>> assignmentIds;
+    private transient volatile List<Collection<UUID>> assignmentIds;
 
     /** Nodes having primary or backup partition assignments. */
     private transient volatile Set<ClusterNode> nodes;
@@ -68,8 +71,8 @@ public class GridAffinityAssignment implements AffinityAssignment, Serializable
      */
     GridAffinityAssignment(AffinityTopologyVersion topVer) {
         this.topVer = topVer;
-        primary = new HashMap<>();
-        backup = new HashMap<>();
+        primary = Collections.emptyMap();
+        backup = Collections.emptyMap();
     }
 
     /**
@@ -77,21 +80,48 @@ public class GridAffinityAssignment implements AffinityAssignment, Serializable
      * @param assignment Assignment.
      * @param idealAssignment Ideal assignment.
      */
-    GridAffinityAssignment(AffinityTopologyVersion topVer,
+    public GridAffinityAssignment(AffinityTopologyVersion topVer,
         List<List<ClusterNode>> assignment,
-        List<List<ClusterNode>> idealAssignment) {
+        List<List<ClusterNode>> idealAssignment
+    ) {
         assert topVer != null;
         assert assignment != null;
         assert idealAssignment != null;
 
         this.topVer = topVer;
-        this.assignment = assignment;
-        this.idealAssignment = idealAssignment.equals(assignment) ? assignment : idealAssignment;
+        this.assignment = Collections.unmodifiableList(assignment);
+        this.idealAssignment = Collections.unmodifiableList(
+            idealAssignment.equals(assignment) ? assignment : idealAssignment
+        );
 
-        primary = new HashMap<>();
-        backup = new HashMap<>();
+        // Temporary mirrors with modifiable partition's collections.
+        Map<UUID, Set<Integer>> tmpPrimary = new HashMap<>();
+        Map<UUID, Set<Integer>> tmpBackup = new HashMap<>();
+        boolean isPrimary;
+
+        for (int partsCnt = assignment.size(), p = 0; p < partsCnt; p++) {
+            isPrimary = true;
+
+            for (ClusterNode node : assignment.get(p)) {
+                UUID id = node.id();
+
+                Map<UUID, Set<Integer>> tmp = isPrimary ? tmpPrimary : tmpBackup;
+
+                /*
+                    https://issues.apache.org/jira/browse/IGNITE-4554 BitSet performs better than HashSet at most cases.
+                    However with 65k partition and high number of nodes (700+) BitSet is loosing HashSet.
+                    We need to replace it with sparse bitsets.
+                 */
+                tmp.computeIfAbsent(id, uuid ->
+                    !IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION ? new BitSetIntSet() : new HashSet<>()
+                ).add(p);
+
+                isPrimary =  false;
+            }
+        }
 
-        initPrimaryBackupMaps();
+        primary = Collections.unmodifiableMap(tmpPrimary);
+        backup = Collections.unmodifiableMap(tmpBackup);
     }
 
     /**
@@ -108,14 +138,14 @@ public class GridAffinityAssignment implements AffinityAssignment, Serializable
     }
 
     /**
-     * @return Affinity assignment computed by affinity function.
+     * @return Unmodifiable ideal affinity assignment computed by affinity function.
      */
     @Override public List<List<ClusterNode>> idealAssignment() {
         return idealAssignment;
     }
 
     /**
-     * @return Affinity assignment.
+     * @return Unmodifiable affinity assignment.
      */
     @Override public List<List<ClusterNode>> assignment() {
         return assignment;
@@ -142,28 +172,39 @@ public class GridAffinityAssignment implements AffinityAssignment, Serializable
     }
 
     /**
-     * Get affinity node IDs for partition.
-     *
+     * Get affinity node IDs for partition as unmodifiable collection.
+     * Depending on AFFINITY_BACKUPS_THRESHOLD we returned newly allocated HashSet or view on List.
      * @param part Partition.
      * @return Affinity nodes IDs.
      */
-    @Override public HashSet<UUID> getIds(int part) {
+    @Override public Collection<UUID> getIds(int part) {
         assert part >= 0 && part < assignment.size() : "Affinity partition is out of range" +
             " [part=" + part + ", partitions=" + assignment.size() + ']';
 
-        List<HashSet<UUID>> assignmentIds0 = assignmentIds;
+        if (IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION)
+            return getOrCreateAssignmentsIds(part);
+        else {
+            List<ClusterNode> nodes = assignment.get(part);
 
-        if (assignmentIds0 == null) {
-            assignmentIds0 = new ArrayList<>();
+            return nodes.size() > GridAffinityAssignment.IGNITE_AFFINITY_BACKUPS_THRESHOLD
+                    ? getOrCreateAssignmentsIds(part)
+                    : F.viewReadOnly(nodes, F.node2id());
+        }
+    }
 
-            for (List<ClusterNode> assignmentPart : assignment) {
-                HashSet<UUID> partIds = new HashSet<>();
+    /**
+     *
+     * @param part Partition ID.
+     * @return Collection of UUIDs.
+     */
+    private Collection<UUID> getOrCreateAssignmentsIds(int part) {
+        List<Collection<UUID>> assignmentIds0 = assignmentIds;
 
-                for (ClusterNode node : assignmentPart)
-                    partIds.add(node.id());
+        if (assignmentIds0 == null) {
+            assignmentIds0 = new ArrayList<>(assignment.size());
 
-                assignmentIds0.add(partIds);
-            }
+            for (List<ClusterNode> assignmentPart : assignment)
+                assignmentIds0.add(assignments2ids(assignmentPart));
 
             assignmentIds = assignmentIds0;
         }
@@ -185,7 +226,7 @@ public class GridAffinityAssignment implements AffinityAssignment, Serializable
                     res.addAll(nodes);
             }
 
-            nodes = res;
+            nodes = Collections.unmodifiableSet(res);
         }
 
         return res;
@@ -205,7 +246,7 @@ public class GridAffinityAssignment implements AffinityAssignment, Serializable
                     res.add(nodes.get(0));
             }
 
-            primaryPartsNodes = res;
+            primaryPartsNodes = Collections.unmodifiableSet(res);
         }
 
         return res;
@@ -220,7 +261,7 @@ public class GridAffinityAssignment implements AffinityAssignment, Serializable
     @Override public Set<Integer> primaryPartitions(UUID nodeId) {
         Set<Integer> set = primary.get(nodeId);
 
-        return set == null ? Collections.<Integer>emptySet() : set;
+        return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
     }
 
     /**
@@ -232,39 +273,7 @@ public class GridAffinityAssignment implements AffinityAssignment, Serializable
     @Override public Set<Integer> backupPartitions(UUID nodeId) {
         Set<Integer> set = backup.get(nodeId);
 
-        return set == null ? Collections.<Integer>emptySet() : set;
-    }
-
-    /**
-     * Initializes primary and backup maps.
-     */
-    private void initPrimaryBackupMaps() {
-        // Temporary mirrors with modifiable partition's collections.
-        Map<UUID, Set<Integer>> tmpPrm = new HashMap<>();
-        Map<UUID, Set<Integer>> tmpBkp = new HashMap<>();
-
-        for (int partsCnt = assignment.size(), p = 0; p < partsCnt; p++) {
-            // Use the first node as primary, other - backups.
-            Map<UUID, Set<Integer>> tmp = tmpPrm;
-            Map<UUID, Set<Integer>> map = primary;
-
-            for (ClusterNode node : assignment.get(p)) {
-                UUID id = node.id();
-
-                Set<Integer> set = tmp.get(id);
-
-                if (set == null) {
-                    tmp.put(id, set = new HashSet<>());
-                    map.put(id, Collections.unmodifiableSet(set));
-                }
-
-                set.add(p);
-
-                // Use the first node as primary, other - backups.
-                tmp = tmpBkp;
-                map = backup;
-            }
-        }
+        return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
     }
 
     /** {@inheritDoc} */
@@ -278,7 +287,7 @@ public class GridAffinityAssignment implements AffinityAssignment, Serializable
         if (o == this)
             return true;
 
-        if (o == null || !(o instanceof AffinityAssignment))
+        if (!(o instanceof AffinityAssignment))
             return false;
 
         return topVer.equals(((AffinityAssignment)o).topologyVersion());
@@ -288,4 +297,4 @@ public class GridAffinityAssignment implements AffinityAssignment, Serializable
     @Override public String toString() {
         return S.toString(GridAffinityAssignment.class, this, super.toString());
     }
-}
\ No newline at end of file
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityUtils.java
index abd5292..9baae93 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/GridAffinityUtils.java
@@ -204,4 +204,4 @@ class GridAffinityUtils {
             topVer = (AffinityTopologyVersion)in.readObject();
         }
     }
-}
\ No newline at end of file
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/HistoryAffinityAssignment.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/HistoryAffinityAssignment.java
index 6bdf7f0..130d2a2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/HistoryAffinityAssignment.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/affinity/HistoryAffinityAssignment.java
@@ -17,14 +17,16 @@
 
 package org.apache.ignite.internal.processors.affinity;
 
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.UUID;
 import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.util.BitSetIntSet;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.S;
-import org.apache.ignite.internal.util.typedef.internal.U;
 
 /**
  *
@@ -44,19 +46,19 @@ public class HistoryAffinityAssignment implements AffinityAssignment {
      * @param assign Assignment.
      */
     HistoryAffinityAssignment(GridAffinityAssignment assign) {
-        this.topVer = assign.topologyVersion();
-        this.assignment = assign.assignment();
-        this.idealAssignment = assign.idealAssignment();
+        topVer = assign.topologyVersion();
+        assignment = assign.assignment();
+        idealAssignment = assign.idealAssignment();
     }
 
     /** {@inheritDoc} */
     @Override public List<List<ClusterNode>> idealAssignment() {
-        return idealAssignment;
+        return Collections.unmodifiableList(idealAssignment);
     }
 
     /** {@inheritDoc} */
     @Override public List<List<ClusterNode>> assignment() {
-        return assignment;
+        return Collections.unmodifiableList(assignment);
     }
 
     /** {@inheritDoc} */
@@ -73,19 +75,20 @@ public class HistoryAffinityAssignment implements AffinityAssignment {
     }
 
     /** {@inheritDoc} */
-    @Override public HashSet<UUID> getIds(int part) {
+    @Override public Collection<UUID> getIds(int part) {
         assert part >= 0 && part < assignment.size() : "Affinity partition is out of range" +
             " [part=" + part + ", partitions=" + assignment.size() + ']';
 
-        List<ClusterNode> nodes = assignment.get(part);
+        if (IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION)
+            return assignments2ids(assignment.get(part));
+        else {
+            List<ClusterNode> nodes = assignment.get(part);
 
-        HashSet<UUID> ids = U.newHashSet(nodes.size());
-
-        for (int i = 0; i < nodes.size(); i++)
-            ids.add(nodes.get(i).id());
-
-        return ids;
-    }
+            return nodes.size() > AffinityAssignment.IGNITE_AFFINITY_BACKUPS_THRESHOLD
+                    ? assignments2ids(nodes)
+                    : F.viewReadOnly(nodes, F.node2id());
+        }
+     }
 
     /** {@inheritDoc} */
     @Override public Set<ClusterNode> nodes() {
@@ -98,7 +101,7 @@ public class HistoryAffinityAssignment implements AffinityAssignment {
                 res.addAll(nodes);
         }
 
-        return res;
+        return Collections.unmodifiableSet(res);
     }
 
     /** {@inheritDoc} */
@@ -112,12 +115,12 @@ public class HistoryAffinityAssignment implements AffinityAssignment {
                 res.add(nodes.get(0));
         }
 
-        return res;
+        return Collections.unmodifiableSet(res);
     }
 
     /** {@inheritDoc} */
     @Override public Set<Integer> primaryPartitions(UUID nodeId) {
-        Set<Integer> res = new HashSet<>();
+        Set<Integer> res = IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION ? new HashSet<>() : new BitSetIntSet();
 
         for (int p = 0; p < assignment.size(); p++) {
             List<ClusterNode> nodes = assignment.get(p);
@@ -126,12 +129,12 @@ public class HistoryAffinityAssignment implements AffinityAssignment {
                 res.add(p);
         }
 
-        return res;
+        return Collections.unmodifiableSet(res);
     }
 
     /** {@inheritDoc} */
     @Override public Set<Integer> backupPartitions(UUID nodeId) {
-        Set<Integer> res = new HashSet<>();
+        Set<Integer> res = IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION ? new HashSet<>() : new BitSetIntSet();
 
         for (int p = 0; p < assignment.size(); p++) {
             List<ClusterNode> nodes = assignment.get(p);
@@ -147,7 +150,7 @@ public class HistoryAffinityAssignment implements AffinityAssignment {
             }
         }
 
-        return res;
+        return Collections.unmodifiableSet(res);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheAffinitySharedManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheAffinitySharedManager.java
index 2c4a640..ed0b7be 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheAffinitySharedManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheAffinitySharedManager.java
@@ -2219,7 +2219,7 @@ public class CacheAffinitySharedManager<K, V> extends GridCacheSharedManagerAdap
         Map<UUID, GridDhtPartitionMap> map = new HashMap<>();
 
         for (int p = 0; p < aff.assignment().size(); p++) {
-            HashSet<UUID> ids = aff.getIds(p);
+            Collection<UUID> ids = aff.getIds(p);
 
             for (UUID nodeId : ids) {
                 GridDhtPartitionMap partMap = map.get(nodeId);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/topology/GridDhtPartitionTopologyImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/topology/GridDhtPartitionTopologyImpl.java
index bb62d64..b0decae 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/topology/GridDhtPartitionTopologyImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/topology/GridDhtPartitionTopologyImpl.java
@@ -1172,7 +1172,7 @@ public class GridDhtPartitionTopologyImpl implements GridDhtPartitionTopology {
             Collection<UUID> diffIds = diffFromAffinity.get(p);
 
             if (!F.isEmpty(diffIds)) {
-                HashSet<UUID> affIds = affAssignment.getIds(p);
+                Collection<UUID> affIds = affAssignment.getIds(p);
 
                 for (UUID nodeId : diffIds) {
                     if (affIds.contains(nodeId)) {
@@ -1229,8 +1229,9 @@ public class GridDhtPartitionTopologyImpl implements GridDhtPartitionTopology {
                 ", node2part=" + node2part + ']';
 
             // Node IDs can be null if both, primary and backup, nodes disappear.
-            List<ClusterNode> nodes = new ArrayList<>();
-
+            // Empirical size to reduce growing of ArrayList.
+            // We bear in mind that most of the time we filter OWNING partitions.
+            List<ClusterNode> nodes = new ArrayList<>(allIds.size() / 2 + 1);
             for (UUID id : allIds) {
                 if (hasState(p, id, state, states)) {
                     ClusterNode n = ctx.discovery().node(id);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/BitSetIntSet.java b/modules/core/src/main/java/org/apache/ignite/internal/util/BitSetIntSet.java
new file mode 100644
index 0000000..47c0198
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/BitSetIntSet.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.internal.util;
+
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Set of Integers implementation based on BitSet.
+ *
+ * Implementation doesn't support negative values and null, cause we can't distinct null from 0 bit in BitSet.
+ */
+public class BitSetIntSet extends GridSerializableCollection<Integer> implements Set<Integer> {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** BitSet. */
+    private final BitSet bitSet;
+
+    /** Calculated size. */
+    private int size;
+
+    /** */
+    public BitSetIntSet() {
+        bitSet = new BitSet();
+    }
+
+    /**
+     *
+     * @param initCap initial capacity.
+     */
+    public BitSetIntSet(int initCap) {
+        bitSet = new BitSet(initCap);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int size() {
+        return size;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean contains(Object o) {
+        if (o == null)
+            throw new UnsupportedOperationException("Null values are not supported!");
+
+        int val = (int)o;
+
+        if (val < 0)
+            throw new UnsupportedOperationException("Negative values are not supported!");
+
+        return bitSet.get(val);
+    }
+
+    /** {@inheritDoc} */
+    @NotNull @Override public Iterator<Integer> iterator() {
+        return new Iterator<Integer>() {
+            private int next = -1;
+
+            /** {@inheritDoc} */
+            @Override public boolean hasNext() {
+                int nextBit = bitSet.nextSetBit(next + 1);
+
+                if (nextBit != -1) {
+                    next = nextBit;
+
+                    return true;
+                }
+                else
+                    return false;
+            }
+
+            /** {@inheritDoc} */
+            @Override public Integer next() {
+                if (next == -1)
+                    throw new NoSuchElementException();
+
+                return next;
+            }
+        };
+    }
+
+    /** Unsupported operation. */
+    @Override public boolean add(Integer integer) {
+        if (integer == null || integer < 0)
+            throw new UnsupportedOperationException("Negative or null values are not supported!");
+
+        boolean alreadySet = bitSet.get(integer);
+
+        if (!alreadySet) {
+            bitSet.set(integer);
+
+            size++;
+        }
+
+        return !alreadySet;
+    }
+
+    /** Unsupported operation. */
+    @Override public boolean remove(Object o) {
+        if (o == null)
+            throw new UnsupportedOperationException("Null values are not supported!");
+
+        int val = (int)o;
+
+        if (val < 0)
+            throw new UnsupportedOperationException("Negative values are not supported!");
+
+        boolean alreadySet = bitSet.get(val);
+
+        if (alreadySet) {
+            bitSet.clear(val);
+
+            size--;
+        }
+
+        return alreadySet;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean containsAll(@NotNull Collection<?> c) {
+        for (Object o : c) {
+            if (!contains(o))
+                return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean addAll(@NotNull Collection<? extends Integer> c) {
+        boolean atLeastOneAdded = false;
+
+        for (Integer o : c) {
+            if (add(o))
+                atLeastOneAdded = true;
+        }
+
+        return atLeastOneAdded;
+    }
+
+    /**
+     * Unsupported operation.
+     */
+    @Override public boolean retainAll(@NotNull Collection<?> c) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean removeAll(@NotNull Collection<?> c) {
+        boolean atLeastOneRemoved = false;
+
+        for (Object o : c) {
+            if (remove(o))
+                atLeastOneRemoved = true;
+        }
+
+        return atLeastOneRemoved;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void clear() {
+        bitSet.clear();
+
+        size = 0;
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignmentTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignmentTest.java
new file mode 100644
index 0000000..fdee72f
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignmentTest.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.affinity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import org.apache.ignite.cache.CacheMetrics;
+import org.apache.ignite.cache.affinity.AffinityFunctionContext;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.cluster.ClusterMetrics;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.events.DiscoveryEvent;
+import org.apache.ignite.internal.util.BitSetIntSet;
+import org.apache.ignite.lang.IgniteProductVersion;
+import org.apache.ignite.spi.discovery.DiscoveryMetricsProvider;
+import org.apache.ignite.spi.discovery.tcp.internal.TcpDiscoveryNode;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.internal.util.reflection.Whitebox;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertSame;
+import static junit.framework.TestCase.assertTrue;
+import static junit.framework.TestCase.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for {@link GridAffinityAssignment}.
+ */
+@RunWith(JUnit4.class)
+public class GridAffinityAssignmentTest {
+    /**  */
+    protected DiscoveryMetricsProvider metrics = new DiscoveryMetricsProvider() {
+        @Override public ClusterMetrics metrics() {
+            return null;
+        }
+
+        @Override public Map<Integer, CacheMetrics> cacheMetrics() {
+            return null;
+        }
+    };
+
+    /** */
+    protected IgniteProductVersion ver = new IgniteProductVersion();
+
+    /**
+     * Test GridAffinityAssignment logic when backup threshold is not reached.
+     */
+    @Test
+    public void testPrimaryBackupPartitions() {
+        ClusterNode clusterNode1 = node(metrics, ver, "1");
+        ClusterNode clusterNode2 = node(metrics, ver, "2");
+        ClusterNode clusterNode3 = node(metrics, ver, "3");
+        ClusterNode clusterNode4 = node(metrics, ver, "4");
+        ClusterNode clusterNode5 = node(metrics, ver, "5");
+        ClusterNode clusterNode6 = node(metrics, ver, "6");
+
+        List<ClusterNode> clusterNodes = new ArrayList<ClusterNode>() {{
+            add(clusterNode1);
+            add(clusterNode2);
+            add(clusterNode3);
+            add(clusterNode4);
+            add(clusterNode5);
+            add(clusterNode6);
+        }};
+
+        GridAffinityAssignment gridAffinityAssignment = new GridAffinityAssignment(
+            new AffinityTopologyVersion(1, 0),
+            new ArrayList<List<ClusterNode>>() {{
+                add(new ArrayList<ClusterNode>() {{
+                    add(clusterNode1);
+                    add(clusterNode2);
+                    add(clusterNode3);
+                    add(clusterNode4);
+                }});
+                add(new ArrayList<ClusterNode>() {{
+                    add(clusterNode1);
+                    add(clusterNode2);
+                    add(clusterNode3);
+                    add(clusterNode4);
+                }});
+                add(new ArrayList<ClusterNode>() {{
+                    add(clusterNode5);
+                    add(clusterNode6);
+                }});
+            }},
+            new ArrayList<>()
+        );
+
+        List<Integer> parts = new ArrayList<Integer>() {{
+            add(0);
+            add(1);
+        }};
+
+        assertTrue(gridAffinityAssignment.primaryPartitions(clusterNode1.id()).containsAll(parts));
+        assertFalse(gridAffinityAssignment.primaryPartitions(clusterNode1.id()).contains(2));
+        assertTrue(gridAffinityAssignment.backupPartitions(clusterNode1.id()).isEmpty());
+
+        for (int i = 1; i < 4; i++) {
+            Set<Integer> primary = gridAffinityAssignment.primaryPartitions(clusterNodes.get(i).id());
+
+            assertTrue(primary.isEmpty());
+
+            Set<Integer> backup = gridAffinityAssignment.backupPartitions(clusterNodes.get(i).id());
+
+            assertTrue(backup.containsAll(parts));
+            assertFalse(backup.contains(2));
+        }
+
+        assertTrue(gridAffinityAssignment.primaryPartitions(clusterNode5.id()).contains(2));
+        assertTrue(gridAffinityAssignment.backupPartitions(clusterNode5.id()).isEmpty());
+
+        assertFalse(gridAffinityAssignment.primaryPartitions(clusterNode6.id()).contains(2));
+        assertTrue(gridAffinityAssignment.backupPartitions(clusterNode6.id()).contains(2));
+
+        assertEquals(4, gridAffinityAssignment.getIds(0).size());
+
+        for (int i = 0; i < 4; i++)
+            assertTrue(gridAffinityAssignment.getIds(0).contains(clusterNodes.get(i).id()));
+
+        if (AffinityAssignment.IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION)
+            assertSame(gridAffinityAssignment.getIds(0), gridAffinityAssignment.getIds(0));
+        else
+            assertNotSame(gridAffinityAssignment.getIds(0), gridAffinityAssignment.getIds(0));
+
+        try {
+            gridAffinityAssignment.primaryPartitions(clusterNode1.id()).add(1000);
+
+            fail("Unmodifiable exception expected");
+        }
+        catch (UnsupportedOperationException ignored) {
+            // Ignored.
+        }
+
+        try {
+            gridAffinityAssignment.backupPartitions(clusterNode1.id()).add(1000);
+
+            fail("Unmodifiable exception expected");
+        }
+        catch (UnsupportedOperationException ignored) {
+            // Ignored.
+        }
+
+        Set<Integer> unwrapped = (Set<Integer>)Whitebox.getInternalState(
+            gridAffinityAssignment.primaryPartitions(clusterNode1.id()),
+            "c"
+        );
+
+        if (AffinityAssignment.IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION)
+            assertTrue(unwrapped instanceof HashSet);
+        else
+            assertTrue(unwrapped instanceof BitSetIntSet);
+    }
+
+    /**
+     * Test GridAffinityAssignment logic when backup threshold is reached. Basically partitioned cache case.
+     */
+    @Test
+    public void testBackupsMoreThanThreshold() {
+        List<ClusterNode> nodes = new ArrayList<>();
+
+        for(int i = 0; i < 10; i++)
+            nodes.add(node(metrics, ver, "1" + i));
+
+        GridAffinityAssignment gridAffinityAssignment = new GridAffinityAssignment(
+            new AffinityTopologyVersion(1, 0),
+            new ArrayList<List<ClusterNode>>() {{
+                add(nodes);
+            }},
+            new ArrayList<>()
+        );
+
+        assertSame(gridAffinityAssignment.getIds(0), gridAffinityAssignment.getIds(0));
+    }
+
+    /**
+     *
+     * @param metrics Metrics.
+     * @param v Version.
+     * @param consistentId ConsistentId.
+     * @return TcpDiscoveryNode.
+     */
+    protected TcpDiscoveryNode node(DiscoveryMetricsProvider metrics, IgniteProductVersion v, String consistentId) {
+        return new TcpDiscoveryNode(
+            UUID.randomUUID(),
+            Collections.singletonList("127.0.0.1"),
+            Collections.singletonList("127.0.0.1"),
+            0,
+            metrics,
+            v,
+            consistentId
+        );
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignmentTestNoOptimizations.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignmentTestNoOptimizations.java
new file mode 100644
index 0000000..425a39c
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignmentTestNoOptimizations.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.affinity;
+
+import org.apache.ignite.IgniteSystemProperties;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link GridAffinityAssignment} without IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION.
+ */
+@RunWith(JUnit4.class)
+public class GridAffinityAssignmentTestNoOptimizations extends GridAffinityAssignmentTest {
+    /** */
+    @BeforeClass
+    public static void beforeTests() {
+        System.setProperty(IgniteSystemProperties.IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION, "true");
+    }
+
+    @AfterClass
+    public static void afterTests() {
+        System.clearProperty(IgniteSystemProperties.IGNITE_DISABLE_AFFINITY_MEMORY_OPTIMIZATION);
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/BitSetIntSetTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/BitSetIntSetTest.java
new file mode 100644
index 0000000..3f21975
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/util/BitSetIntSetTest.java
@@ -0,0 +1,339 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ *
+ */
+@RunWith(JUnit4.class)
+public class BitSetIntSetTest extends GridCommonAbstractTest {
+    /**
+     *
+     */
+    @Test
+    public void testSizeIsEmpty() {
+        sizeIsEmpty(0);
+        sizeIsEmpty(1024);
+    }
+
+    /**
+     *
+     */
+    private void sizeIsEmpty(int initCap) {
+        BitSetIntSet bitSetIntSet = initCap != 0 ? new BitSetIntSet(initCap) : new BitSetIntSet();
+
+        assertEquals(0, bitSetIntSet.size());
+        assertTrue(bitSetIntSet.isEmpty());
+
+        bitSetIntSet = new BitSetIntSet();
+
+        assertTrue(bitSetIntSet.add(1));
+        assertEquals(1, bitSetIntSet.size());
+        assertTrue(bitSetIntSet.add(10));
+        assertEquals(2, bitSetIntSet.size());
+        assertTrue(bitSetIntSet.add(1025));
+        assertEquals(3, bitSetIntSet.size());
+
+        assertEquals(3, bitSetIntSet.size());
+        assertFalse(bitSetIntSet.isEmpty());
+    }
+
+    /** */
+    @Test
+    public void testItetator() {
+        testIterator(0);
+        testIterator(1024);
+    }
+
+    /** */
+    private void testIterator(int initCap) {
+        BitSetIntSet bitSet = initCap != 0 ? new BitSetIntSet(initCap) : new BitSetIntSet();
+
+        assertTrue(bitSet.add(0));
+        assertTrue(bitSet.add(1));
+
+        assertTrue(bitSet.add(10));
+        assertFalse(bitSet.add(10));
+
+        assertTrue(bitSet.add(11));
+        assertTrue(bitSet.add(1025));
+
+        Iterator<Integer> iterator = bitSet.iterator();
+
+        assertTrue(iterator.hasNext());
+        assertEquals(0, (int)iterator.next());
+
+        assertTrue(iterator.hasNext());
+        assertEquals(1, (int)iterator.next());
+
+        assertTrue(iterator.hasNext());
+        assertEquals(10, (int)iterator.next());
+
+        assertTrue(iterator.hasNext());
+        assertEquals(11, (int)iterator.next());
+
+        assertTrue(iterator.hasNext());
+        assertEquals(1025, (int)iterator.next());
+
+        assertFalse(iterator.hasNext());
+
+        List<Integer> list = new ArrayList<>();
+
+        for(Integer i : bitSet)
+            list.add(i);
+
+        assertEquals(5, list.size());
+
+        assertEquals(0, (int)list.get(0));
+        assertEquals(1, (int)list.get(1));
+        assertEquals(10, (int)list.get(2));
+        assertEquals(11, (int)list.get(3));
+        assertEquals(1025, (int)list.get(4));
+
+
+        assertFalse(bitSet.remove(2));
+        assertEquals(5, bitSet.size());
+
+        assertTrue(bitSet.remove(1));
+        assertFalse(bitSet.remove(1));
+        assertEquals(4, bitSet.size());
+        assertFalse(bitSet.isEmpty());
+
+        assertTrue(bitSet.remove(10));
+        assertFalse(bitSet.remove(10));
+        assertEquals(3, bitSet.size());
+        assertFalse(bitSet.isEmpty());
+    }
+
+    /** */
+    @Test
+    public void testContains() {
+        BitSetIntSet bitSetInt = new BitSetIntSet();
+
+        bitSetInt.add(1);
+        bitSetInt.add(10);
+        bitSetInt.add(10);
+        bitSetInt.add(11);
+        bitSetInt.add(1025);
+
+
+        assertTrue(bitSetInt.contains(1));
+        assertFalse(bitSetInt.contains(2));
+        assertFalse(bitSetInt.contains(3));
+        assertFalse(bitSetInt.contains(4));
+        assertTrue(bitSetInt.contains(10));
+        assertTrue(bitSetInt.contains(11));
+        assertFalse(bitSetInt.contains(1024));
+        assertTrue(bitSetInt.contains(1025));
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testContainsAll() {
+        BitSetIntSet bitSetInt = new BitSetIntSet();
+
+        bitSetInt.add(1);
+        bitSetInt.add(10);
+        bitSetInt.add(10);
+        bitSetInt.add(11);
+        bitSetInt.add(1025);
+
+        assertTrue(bitSetInt.containsAll(new ArrayList<Integer>() {{
+            add(1);
+            add(10);
+        }}));
+
+        assertFalse(bitSetInt.containsAll(new ArrayList<Integer>() {{
+            add(1);
+            add(10);
+            add(11);
+            add(1025);
+            add(1026);
+        }}));
+
+        assertFalse(bitSetInt.containsAll(new ArrayList<Integer>() {{
+            add(1);
+            add(10);
+            add(12);
+        }}));
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testAddAllRemoveAllRetainAll() {
+        BitSetIntSet bitSetInt = new BitSetIntSet();
+
+        bitSetInt.add(1);
+        bitSetInt.add(10);
+        bitSetInt.add(10);
+        bitSetInt.add(11);
+        bitSetInt.add(1025);
+
+        assertFalse(bitSetInt.addAll(new ArrayList<Integer>() {{
+            add(1);
+            add(10);
+        }}));
+
+        assertEquals(4, bitSetInt.size());
+
+        assertTrue(bitSetInt.addAll(new ArrayList<Integer>() {{
+            add(1);
+            add(10);
+            add(11);
+            add(1025);
+            add(1026);
+        }}));
+
+        assertEquals(5, bitSetInt.size());
+
+        try {
+            bitSetInt.retainAll(new ArrayList<Integer>() {{
+                add(10);
+                add(1025);
+            }});
+
+            fail("retainAll is not supported");
+        }
+        catch (UnsupportedOperationException ignored) {
+            // Ignored.
+        }
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testToArray() {
+        BitSetIntSet bitSetInt = new BitSetIntSet();
+
+        assertEquals(0, bitSetInt.toArray().length);
+
+        bitSetInt.add(1);
+        bitSetInt.add(10);
+        bitSetInt.add(10);
+        bitSetInt.add(11);
+        bitSetInt.add(1025);
+
+        Object[] arr = bitSetInt.toArray();
+
+        assertEquals(4, arr.length);
+
+        assertEquals(1, (int)arr[0]);
+        assertEquals(10, (int)arr[1]);
+        assertEquals(11, (int)arr[2]);
+        assertEquals(1025, (int)arr[3]);
+
+        Integer[] input = new Integer[1];
+
+        Integer[] output = bitSetInt.toArray(input);
+
+        assertNotSame(input, output);
+
+        assertEquals(4, arr.length);
+
+        assertEquals(1, arr[0]);
+        assertEquals(10, arr[1]);
+        assertEquals(11, arr[2]);
+        assertEquals(1025, arr[3]);
+
+        input = new Integer[6];
+
+        output = bitSetInt.toArray(input);
+
+        assertSame(input, output);
+
+        assertEquals(6, output.length);
+
+        assertEquals(1, (int)output[0]);
+        assertEquals(10, (int)output[1]);
+        assertEquals(11, (int)output[2]);
+        assertEquals(1025, (int)output[3]);
+        assertNull(output[4]);
+        assertNull(output[5]);
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testInvalidValues() {
+        BitSetIntSet bitSetInt = new BitSetIntSet();
+
+        try {
+            bitSetInt.add(null);
+            fail("add should fail here");
+        }
+        catch (UnsupportedOperationException ignored) {
+            // Ignored.
+        }
+
+        try {
+            bitSetInt.add(-1);
+            fail("add should fail here");
+        }
+        catch (UnsupportedOperationException ignored) {
+            // Ignored.
+        }
+
+        try {
+            bitSetInt.contains(null);
+            fail("contains should fail here");
+        }
+        catch (UnsupportedOperationException ignored) {
+            // Ignored.
+        }
+
+        try {
+            bitSetInt.contains(-1);
+            fail("contains should fail here");
+        }
+        catch (UnsupportedOperationException ignored) {
+            // Ignored.
+        }
+
+        try {
+            bitSetInt.remove(null);
+            fail("remove should fail here");
+        }
+        catch (UnsupportedOperationException ignored) {
+            // Ignored.
+        }
+
+        try {
+            bitSetInt.remove(-1);
+            fail("remove should fail here");
+        }
+        catch (UnsupportedOperationException ignored) {
+            // Ignored.
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java
index d6ab28d..b55f140 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java
@@ -42,6 +42,8 @@ import org.apache.ignite.internal.MarshallerContextLockingSelfTest;
 import org.apache.ignite.internal.TransactionsMXBeanImplTest;
 import org.apache.ignite.internal.managers.IgniteDiagnosticMessagesMultipleConnectionsTest;
 import org.apache.ignite.internal.managers.IgniteDiagnosticMessagesTest;
+import org.apache.ignite.internal.processors.affinity.GridAffinityAssignmentTest;
+import org.apache.ignite.internal.processors.affinity.GridAffinityAssignmentTestNoOptimizations;
 import org.apache.ignite.internal.processors.affinity.GridAffinityProcessorMemoryLeakTest;
 import org.apache.ignite.internal.processors.affinity.GridAffinityProcessorRendezvousSelfTest;
 import org.apache.ignite.internal.processors.cache.CacheLocalGetSerializationTest;
@@ -72,6 +74,7 @@ import org.apache.ignite.internal.processors.database.SwapPathConstructionSelfTe
 import org.apache.ignite.internal.processors.odbc.OdbcConfigurationValidationSelfTest;
 import org.apache.ignite.internal.processors.odbc.OdbcEscapeSequenceSelfTest;
 import org.apache.ignite.internal.product.GridProductVersionSelfTest;
+import org.apache.ignite.internal.util.BitSetIntSetTest;
 import org.apache.ignite.internal.util.GridCleanerTest;
 import org.apache.ignite.internal.util.nio.IgniteExceptionInNioWorkerSelfTest;
 import org.apache.ignite.marshaller.DynamicProxySerializationMultiJvmSelfTest;
@@ -120,6 +123,8 @@ import org.junit.runners.Suite;
 
     GridReleaseTypeSelfTest.class,
     GridProductVersionSelfTest.class,
+    GridAffinityAssignmentTest.class,
+    GridAffinityAssignmentTestNoOptimizations.class,
     GridAffinityProcessorRendezvousSelfTest.class,
     GridAffinityProcessorMemoryLeakTest.class,
     GridClosureProcessorSelfTest.class,
@@ -174,6 +179,7 @@ import org.junit.runners.Suite;
     CacheFreeListImplSelfTest.class,
     DataRegionMetricsSelfTest.class,
     SwapPathConstructionSelfTest.class,
+    BitSetIntSetTest.class,
 
     IgniteMarshallerCacheFSRestoreTest.class,
     IgniteMarshallerCacheClassNameConflictTest.class,