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

frankgh pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new ce53702764 ninja trunk patch for CASSANDRA-21587
ce53702764 is described below

commit ce5370276452db4e5981af48839861ef4911cbe2
Author: Francisco Guerrero <[email protected]>
AuthorDate: Thu Sep 24 00:12:07 2026 +0000

    ninja trunk patch for CASSANDRA-21587
    
    this diff was supposed to be included in 
0691eb3319e5fc33a8d393f48120bb7b3ca901da
    
    Avoid megamorphic List.get in ClusteringPrefix.Deserializer
    
    Carry clustering types as AbstractType<?>[] instead of List, so the hot
    path does an array load with no virtual dispatch and ClusteringComparator
    needs no defensive copy.
    
     patch by Taejin Koo and Francisco Guerrero; reviewed by Taejin Koo, Dmitry 
Konstantinov for CASSANDRA-21587
    
    Co-authored-by: koo.taejin <[email protected]>
---
 src/java/org/apache/cassandra/db/Clustering.java   |  25 ++-
 .../cassandra/db/ClusteringBoundOrBoundary.java    |  11 +-
 .../apache/cassandra/db/ClusteringComparator.java  |  34 ++---
 .../org/apache/cassandra/db/ClusteringPrefix.java  |  44 +++---
 .../apache/cassandra/db/SerializationHeader.java   |  35 +++--
 src/java/org/apache/cassandra/db/Slice.java        |   9 +-
 src/java/org/apache/cassandra/db/Slices.java       |  16 +-
 .../cassandra/db/compaction/CursorCompactor.java   |   2 +-
 .../cassandra/db/compaction/StatefulCursor.java    |   3 +-
 .../apache/cassandra/db/marshal/AbstractType.java  |   2 +
 .../cassandra/io/sstable/ClusteringDescriptor.java |   3 +-
 .../org/apache/cassandra/io/sstable/IndexInfo.java |   5 +-
 .../cassandra/io/sstable/SSTableCursorWriter.java  |   2 -
 .../io/sstable/metadata/MetadataCollector.java     |   9 +-
 .../io/sstable/metadata/StatsMetadata.java         |  15 +-
 .../org/apache/cassandra/schema/TableMetadata.java |  15 +-
 .../serializers/AbstractTypeSerializer.java        |  27 ++--
 .../cassandra/service/pager/PagingState.java       |   8 +-
 .../cassandra/tools/SSTableMetadataViewer.java     |   6 +-
 src/java/org/apache/cassandra/tools/Util.java      |   8 +-
 test/bin/jmh                                       |   9 +-
 .../cassandra/harry/model/BytesPartitionState.java |   2 +-
 .../ClusteringPrefixDeserializerBench.java         | 169 +++++++++++++++++++++
 .../microbench/sstable/SSTableCursorPipeUtil.java  |   3 +-
 .../sstable/SSTableReadingFileCursorBench.java     |   3 +-
 .../cassandra/db/ClusteringComparatorTest.java     |  64 +++++++-
 .../cassandra/db/SerializationHeaderTest.java      |  90 +++++++++++
 .../db/compaction/StatefulCursorTest.java          |   2 +-
 .../differential/ComplexColumnCursorReadTest.java  |   5 +-
 .../org/apache/cassandra/db/filter/SliceTest.java  |  34 ++---
 .../compress/CompressedSequentialWriterTest.java   |   4 +-
 .../DirectCompressedSequentialWriterTest.java      |   3 +-
 .../SSTableCursorReaderCopyCellValueTest.java      |   3 +-
 .../SSTableCursorReaderCorruptCellCountTest.java   |   5 +-
 .../DataComponentDirectWriteSelectionTest.java     |   3 +-
 .../io/sstable/format/big/RowIndexEntryTest.java   |   4 +-
 .../serializers/AbstractTypeSerializerTest.java    |  63 ++++++++
 .../bytecomparable/ByteSourceSequenceTest.java     |  29 ++--
 38 files changed, 574 insertions(+), 200 deletions(-)

diff --git a/src/java/org/apache/cassandra/db/Clustering.java 
b/src/java/org/apache/cassandra/db/Clustering.java
index 9609aaab1f..a8b7d954cb 100644
--- a/src/java/org/apache/cassandra/db/Clustering.java
+++ b/src/java/org/apache/cassandra/db/Clustering.java
@@ -20,7 +20,6 @@ package org.apache.cassandra.db;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.List;
 
 import org.apache.cassandra.cache.IMeasurableMemory;
 import org.apache.cassandra.db.marshal.AbstractType;
@@ -138,14 +137,14 @@ public interface Clustering<V> extends 
ClusteringPrefix<V>, IMeasurableMemory
      */
     public static class Serializer
     {
-        public void serialize(Clustering<?> clustering, DataOutputPlus out, 
int version, List<AbstractType<?>> types) throws IOException
+        public void serialize(Clustering<?> clustering, DataOutputPlus out, 
int version, AbstractType<?>[] types) throws IOException
         {
             assert clustering != STATIC_CLUSTERING : "We should never 
serialize a static clustering";
-            assert clustering.size() == types.size() : "Invalid clustering for 
the table: " + clustering;
+            assert clustering.size() == types.length : "Invalid clustering for 
the table: " + clustering;
             ClusteringPrefix.serializer.serializeValuesWithoutSize(clustering, 
out, version, types);
         }
 
-        public ByteBuffer serialize(Clustering<?> clustering, int version, 
List<AbstractType<?>> types)
+        public ByteBuffer serialize(Clustering<?> clustering, int version, 
AbstractType<?>[] types)
         {
             try (DataOutputBuffer buffer = new 
DataOutputBuffer((int)serializedSize(clustering, version, types)))
             {
@@ -158,27 +157,27 @@ public interface Clustering<V> extends 
ClusteringPrefix<V>, IMeasurableMemory
             }
         }
 
-        public long serializedSize(Clustering<?> clustering, int version, 
List<AbstractType<?>> types)
+        public long serializedSize(Clustering<?> clustering, int version, 
AbstractType<?>[] types)
         {
             return 
ClusteringPrefix.serializer.valuesWithoutSizeSerializedSize(clustering, 
version, types);
         }
 
-        public void skip(DataInputPlus in, int version, List<AbstractType<?>> 
types) throws IOException
+        public void skip(DataInputPlus in, int version, AbstractType<?>[] 
types) throws IOException
         {
-            if (!types.isEmpty())
-                ClusteringPrefix.serializer.skipValuesWithoutSize(in, 
types.size(), version, types);
+            if (types.length > 0)
+                ClusteringPrefix.serializer.skipValuesWithoutSize(in, 
types.length, version, types);
         }
 
-        public Clustering<byte[]> deserialize(DataInputPlus in, int version, 
List<AbstractType<?>> types) throws IOException
+        public Clustering<byte[]> deserialize(DataInputPlus in, int version, 
AbstractType<?>[] types) throws IOException
         {
-            if (types.isEmpty())
+            if (types.length == 0)
                 return ByteArrayAccessor.factory.clustering();
 
-            byte[][] values = 
ClusteringPrefix.serializer.deserializeValuesWithoutSize(in, types.size(), 
version, types);
+            byte[][] values = 
ClusteringPrefix.serializer.deserializeValuesWithoutSize(in, types.length, 
version, types);
             return ByteArrayAccessor.factory.clustering(values);
         }
 
-        public Clustering<byte[]> deserialize(ByteBuffer in, int version, 
List<AbstractType<?>> types)
+        public Clustering<byte[]> deserialize(ByteBuffer in, int version, 
AbstractType<?>[] types)
         {
             try (DataInputBuffer buffer = new DataInputBuffer(in, true))
             {
@@ -197,4 +196,4 @@ public interface Clustering<V> extends ClusteringPrefix<V>, 
IMeasurableMemory
      */
 
     Clustering<?> ensureAccessorFactorySupport();
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/db/ClusteringBoundOrBoundary.java 
b/src/java/org/apache/cassandra/db/ClusteringBoundOrBoundary.java
index 1a13ba2567..969908e15b 100644
--- a/src/java/org/apache/cassandra/db/ClusteringBoundOrBoundary.java
+++ b/src/java/org/apache/cassandra/db/ClusteringBoundOrBoundary.java
@@ -22,7 +22,6 @@ package org.apache.cassandra.db;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.List;
 
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.ByteArrayAccessor;
@@ -100,27 +99,27 @@ public interface ClusteringBoundOrBoundary<V> extends 
ClusteringPrefix<V>
 
     public static class Serializer
     {
-        public <T> void serialize(ClusteringBoundOrBoundary<T> bound, 
DataOutputPlus out, int version, List<AbstractType<?>> types) throws IOException
+        public <T> void serialize(ClusteringBoundOrBoundary<T> bound, 
DataOutputPlus out, int version, AbstractType<?>[] types) throws IOException
         {
             out.writeByte(bound.kind().ordinal());
             out.writeShort(bound.size());
             ClusteringPrefix.serializer.serializeValuesWithoutSize(bound, out, 
version, types);
         }
 
-        public <T> long serializedSize(ClusteringBoundOrBoundary<T> bound, int 
version, List<AbstractType<?>> types)
+        public <T> long serializedSize(ClusteringBoundOrBoundary<T> bound, int 
version, AbstractType<?>[] types)
         {
             return 1 // kind ordinal
                  + TypeSizes.sizeof((short)bound.size())
                  + 
ClusteringPrefix.serializer.valuesWithoutSizeSerializedSize(bound, version, 
types);
         }
 
-        public ClusteringBoundOrBoundary<byte[]> deserialize(DataInputPlus in, 
int version, List<AbstractType<?>> types) throws IOException
+        public ClusteringBoundOrBoundary<byte[]> deserialize(DataInputPlus in, 
int version, AbstractType<?>[] types) throws IOException
         {
             Kind kind = Kind.fromOrdinal(in.readByte());
             return deserializeValues(in, kind, version, types);
         }
 
-        public void skipValues(DataInputPlus in, Kind kind, int version, 
List<AbstractType<?>> types) throws IOException
+        public void skipValues(DataInputPlus in, Kind kind, int version, 
AbstractType<?>[] types) throws IOException
         {
             int size = in.readUnsignedShort();
             if (size == 0)
@@ -129,7 +128,7 @@ public interface ClusteringBoundOrBoundary<V> extends 
ClusteringPrefix<V>
             ClusteringPrefix.serializer.skipValuesWithoutSize(in, size, 
version, types);
         }
 
-        public ClusteringBoundOrBoundary<byte[]> 
deserializeValues(DataInputPlus in, Kind kind, int version, 
List<AbstractType<?>> types) throws IOException
+        public ClusteringBoundOrBoundary<byte[]> 
deserializeValues(DataInputPlus in, Kind kind, int version, AbstractType<?>[] 
types) throws IOException
         {
             int size = in.readUnsignedShort();
             if (size == 0)
diff --git a/src/java/org/apache/cassandra/db/ClusteringComparator.java 
b/src/java/org/apache/cassandra/db/ClusteringComparator.java
index 88ba6e4fc5..746376b0ee 100644
--- a/src/java/org/apache/cassandra/db/ClusteringComparator.java
+++ b/src/java/org/apache/cassandra/db/ClusteringComparator.java
@@ -20,11 +20,8 @@ package org.apache.cassandra.db;
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.Comparator;
-import java.util.List;
-import java.util.Objects;
 
 import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableList;
 
 import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.ByteBufferAccessor;
@@ -38,6 +35,7 @@ import 
org.apache.cassandra.utils.bytecomparable.ByteComparable;
 import org.apache.cassandra.utils.bytecomparable.ByteSource;
 import org.apache.cassandra.utils.vint.VIntCoding;
 
+import static 
org.apache.cassandra.db.marshal.AbstractType.EMPTY_ABSTRACT_TYPE_ARRAY;
 import static org.apache.cassandra.utils.bytecomparable.ByteSource.EXCLUDED;
 import static 
org.apache.cassandra.utils.bytecomparable.ByteSource.NEXT_COMPONENT;
 import static 
org.apache.cassandra.utils.bytecomparable.ByteSource.NEXT_COMPONENT_EMPTY;
@@ -54,7 +52,7 @@ import static 
org.apache.cassandra.utils.bytecomparable.ByteSource.TERMINATOR;
  */
 public class ClusteringComparator implements Comparator<Clusterable>
 {
-    private final List<AbstractType<?>> clusteringTypes;
+    private final AbstractType<?>[] clusteringTypes;
 
     private final Comparator<IndexInfo> indexComparator;
     private final Comparator<IndexInfo> indexReverseComparator;
@@ -65,20 +63,15 @@ public class ClusteringComparator implements 
Comparator<Clusterable>
 
     public ClusteringComparator(AbstractType<?>... clusteringTypes)
     {
-        this(ImmutableList.copyOf(clusteringTypes));
-    }
-
-    public ClusteringComparator(Iterable<AbstractType<?>> clusteringTypes)
-    {
-        // copy the list to ensure despatch is monomorphic
-        this.clusteringTypes = ImmutableList.copyOf(clusteringTypes);
+        // No defensive copy: callers must not retain/mutate the array after 
construction
+        this.clusteringTypes = clusteringTypes.length == 0 ? 
EMPTY_ABSTRACT_TYPE_ARRAY : clusteringTypes;
 
         this.indexComparator = (o1, o2) -> 
ClusteringComparator.this.compare((ClusteringPrefix<?>) o1.lastName,
                                                                              
(ClusteringPrefix<?>) o2.lastName);
         this.indexReverseComparator = (o1, o2) -> 
ClusteringComparator.this.compare((ClusteringPrefix<?>) o1.firstName,
                                                                                
     (ClusteringPrefix<?>) o2.firstName);
         this.reverseComparator = (c1, c2) -> 
ClusteringComparator.this.compare(c2, c1);
-        for (AbstractType<?> type : clusteringTypes)
+        for (AbstractType<?> type : this.clusteringTypes)
             type.checkComparable(); // this should already be enforced by 
TableMetadata.Builder.addColumn, but we check again for other constructors
     }
 
@@ -87,14 +80,17 @@ public class ClusteringComparator implements 
Comparator<Clusterable>
      */
     public int size()
     {
-        return clusteringTypes.size();
+        return clusteringTypes.length;
     }
 
     /**
      * The "subtypes" of this clustering comparator, that is the types of the 
clustering
      * columns for the table this is a comparator of.
+     *
+     * @return the internal array directly (no copy), shared with every 
consumer of this comparator
+     * as it is read on the serialization hot path; it must not be mutated.
      */
-    public List<AbstractType<?>> subtypes()
+    public AbstractType<?>[] subtypes()
     {
         return clusteringTypes;
     }
@@ -104,7 +100,7 @@ public class ClusteringComparator implements 
Comparator<Clusterable>
      */
     public AbstractType<?> subtype(int i)
     {
-        return clusteringTypes.get(i);
+        return clusteringTypes[i];
     }
 
     /**
@@ -303,7 +299,7 @@ public class ClusteringComparator implements 
Comparator<Clusterable>
         if (v2 == null)
             return 1;
 
-        return clusteringTypes.get(i).compare(v1, accessor1, v2, accessor2);
+        return clusteringTypes[i].compare(v1, accessor1, v2, accessor2);
     }
 
     public <V1, V2> int compareComponent(int i, ClusteringPrefix<V1> v1, 
ClusteringPrefix<V2> v2)
@@ -664,12 +660,12 @@ public class ClusteringComparator implements 
Comparator<Clusterable>
             return false;
 
         ClusteringComparator that = (ClusteringComparator)o;
-        return this.clusteringTypes.equals(that.clusteringTypes);
+        return Arrays.equals(this.clusteringTypes, that.clusteringTypes);
     }
 
     @Override
     public int hashCode()
     {
-        return Objects.hashCode(clusteringTypes);
+        return Arrays.hashCode(clusteringTypes);
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/db/ClusteringPrefix.java 
b/src/java/org/apache/cassandra/db/ClusteringPrefix.java
index ff2b9261a2..913510521d 100644
--- a/src/java/org/apache/cassandra/db/ClusteringPrefix.java
+++ b/src/java/org/apache/cassandra/db/ClusteringPrefix.java
@@ -19,7 +19,6 @@ package org.apache.cassandra.db;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.List;
 import java.util.Objects;
 import java.util.function.ToIntFunction;
 
@@ -431,7 +430,7 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
      * Produce a human-readable representation of the clustering given the 
list of types.
      * Easier to access than metadata for debugging.
      */
-    public default String clusteringString(List<AbstractType<?>> types)
+    public default String clusteringString(AbstractType<?>[] types)
     {
         StringBuilder sb = new StringBuilder();
         sb.append(kind()).append('(');
@@ -439,7 +438,7 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
         {
             if (i > 0)
                 sb.append(", ");
-            sb.append(types.get(i).getString(get(i), accessor()));
+            sb.append(types[i].getString(get(i), accessor()));
         }
         return sb.append(')').toString();
     }
@@ -466,7 +465,7 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
 
     public static class Serializer
     {
-        public void serialize(ClusteringPrefix<?> clustering, DataOutputPlus 
out, int version, List<AbstractType<?>> types) throws IOException
+        public void serialize(ClusteringPrefix<?> clustering, DataOutputPlus 
out, int version, AbstractType<?>[] types) throws IOException
         {
             // We shouldn't serialize static clusterings
             assert clustering.kind() != Kind.STATIC_CLUSTERING;
@@ -481,7 +480,7 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
             }
         }
 
-        public void skip(DataInputPlus in, int version, List<AbstractType<?>> 
types) throws IOException
+        public void skip(DataInputPlus in, int version, AbstractType<?>[] 
types) throws IOException
         {
             Kind kind = Kind.fromOrdinal(in.readByte());
             // We shouldn't serialize static clusterings
@@ -492,7 +491,7 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
                 ClusteringBoundOrBoundary.serializer.skipValues(in, kind, 
version, types);
         }
 
-        public ClusteringPrefix<byte[]> deserialize(DataInputPlus in, int 
version, List<AbstractType<?>> types) throws IOException
+        public ClusteringPrefix<byte[]> deserialize(DataInputPlus in, int 
version, AbstractType<?>[] types) throws IOException
         {
             Kind kind = Kind.fromOrdinal(in.readByte());
             // We shouldn't serialize static clusterings
@@ -503,7 +502,7 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
                 return 
ClusteringBoundOrBoundary.serializer.deserializeValues(in, kind, version, 
types);
         }
 
-        public long serializedSize(ClusteringPrefix<?> clustering, int 
version, List<AbstractType<?>> types)
+        public long serializedSize(ClusteringPrefix<?> clustering, int 
version, AbstractType<?>[] types)
         {
             // We shouldn't serialize static clusterings
             assert clustering.kind() != Kind.STATIC_CLUSTERING;
@@ -513,7 +512,7 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
                 return 
ClusteringBoundOrBoundary.serializer.serializedSize((ClusteringBoundOrBoundary<?>)clustering,
 version, types);
         }
 
-        <V> void serializeValuesWithoutSize(ClusteringPrefix<V> clustering, 
DataOutputPlus out, int version, List<AbstractType<?>> types) throws IOException
+        <V> void serializeValuesWithoutSize(ClusteringPrefix<V> clustering, 
DataOutputPlus out, int version, AbstractType<?>[] types) throws IOException
         {
             int offset = 0;
             int clusteringSize = clustering.size();
@@ -528,13 +527,13 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
                 out.writeUnsignedVInt(makeHeader(clustering, offset, limit));
                 while (offset < limit)
                 {
-                    
clustering.writeValueSkippingNullAndEmpty(types.get(offset), offset, out);
+                    clustering.writeValueSkippingNullAndEmpty(types[offset], 
offset, out);
                     offset++;
                 }
             }
         }
 
-        <V> long valuesWithoutSizeSerializedSize(ClusteringPrefix<V> 
clustering, int version, List<AbstractType<?>> types)
+        <V> long valuesWithoutSizeSerializedSize(ClusteringPrefix<V> 
clustering, int version, AbstractType<?>[] types)
         {
             long result = 0;
             int offset = 0;
@@ -547,12 +546,12 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
             }
             for (int i = 0; i < clusteringSize; i++)
             {
-                result += 
clustering.writtenLengthSkippingNullAndEmpty(types.get(i), i);
+                result += 
clustering.writtenLengthSkippingNullAndEmpty(types[i], i);
             }
             return result;
         }
 
-        public byte[][] deserializeValuesWithoutSize(DataInputPlus in, int 
size, int version, List<AbstractType<?>> types) throws IOException
+        public byte[][] deserializeValuesWithoutSize(DataInputPlus in, int 
size, int version, AbstractType<?>[] types) throws IOException
         {
             // Callers of this method should handle the case where size = 0 
(in all case we want to return a special value anyway).
             assert size > 0;
@@ -567,14 +566,14 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
                     values[offset] = isNull(header, offset)
                                      ? null
                                      : (isEmpty(header, offset) ? 
ByteArrayUtil.EMPTY_BYTE_ARRAY
-                                                                : 
types.get(offset).readArray(in, DatabaseDescriptor.getMaxValueSize()));
+                                                                : 
types[offset].readArray(in, DatabaseDescriptor.getMaxValueSize()));
                     offset++;
                 }
             }
             return values;
         }
 
-        void skipValuesWithoutSize(DataInputPlus in, int size, int version, 
List<AbstractType<?>> types) throws IOException
+        void skipValuesWithoutSize(DataInputPlus in, int size, int version, 
AbstractType<?>[] types) throws IOException
         {
             // Callers of this method should handle the case where size = 0 
(in all case we want to return a special value anyway).
             assert size > 0;
@@ -586,7 +585,7 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
                 while (offset < limit)
                 {
                     if (!isNull(header, offset) && !isEmpty(header, offset))
-                         types.get(offset).skipValue(in);
+                         types[offset].skipValue(in);
                     offset++;
                 }
             }
@@ -704,13 +703,14 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
             if (i >= nextSize)
                 return false;
 
+            AbstractType<?>[] clusteringTypes = 
serializationHeader.clusteringTypes();
             while (deserializedSize <= i)
-                deserializeOne();
+                deserializeOne(clusteringTypes);
 
             return true;
         }
 
-        private boolean deserializeOne() throws IOException
+        private boolean deserializeOne(AbstractType<?>[] clusteringTypes) 
throws IOException
         {
             if (deserializedSize == nextSize)
                 return false;
@@ -722,13 +722,14 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
             nextValues[i] = Serializer.isNull(nextHeader, i)
                           ? null
                           : (Serializer.isEmpty(nextHeader, i) ? 
ByteArrayUtil.EMPTY_BYTE_ARRAY
-                                                               : 
serializationHeader.clusteringTypes().get(i).readArray(in, 
DatabaseDescriptor.getMaxValueSize()));
+                                                               : 
clusteringTypes[i].readArray(in, DatabaseDescriptor.getMaxValueSize()));
             return true;
         }
 
         private void deserializeAll() throws IOException
         {
-            while (deserializeOne())
+            AbstractType<?>[] clusteringTypes = 
serializationHeader.clusteringTypes();
+            while (deserializeOne(clusteringTypes))
                 continue;
         }
 
@@ -752,12 +753,13 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
 
         public ClusteringPrefix.Kind skipNext() throws IOException
         {
+            AbstractType<?>[] clusteringTypes = 
serializationHeader.clusteringTypes();
             for (int i = deserializedSize; i < nextSize; i++)
             {
                 if ((i % 32) == 0)
                     nextHeader = in.readUnsignedVInt();
                 if (!Serializer.isNull(nextHeader, i) && 
!Serializer.isEmpty(nextHeader, i))
-                    serializationHeader.clusteringTypes().get(i).skipValue(in);
+                    clusteringTypes[i].skipValue(in);
             }
             deserializedSize = nextSize;
             return nextKind;
@@ -808,4 +810,4 @@ public interface ClusteringPrefix<V> extends 
IMeasurableMemory, Clusterable<V>
         
comparator.subtype(clusterIndex).checkConstraints(accessor().toBuffer(get(clusterIndex)),
 constraints);
     }
 
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/db/SerializationHeader.java 
b/src/java/org/apache/cassandra/db/SerializationHeader.java
index dd4ea5a495..4e10d5f968 100644
--- a/src/java/org/apache/cassandra/db/SerializationHeader.java
+++ b/src/java/org/apache/cassandra/db/SerializationHeader.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.db;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -56,7 +57,7 @@ public class SerializationHeader
     private final boolean isForSSTable;
 
     private final AbstractType<?> keyType;
-    private final List<AbstractType<?>> clusteringTypes;
+    private final AbstractType<?>[] clusteringTypes;
 
     private final RegularAndStaticColumns columns;
     private final EncodingStats stats;
@@ -67,7 +68,7 @@ public class SerializationHeader
 
     private SerializationHeader(boolean isForSSTable,
                                 AbstractType<?> keyType,
-                                List<AbstractType<?>> clusteringTypes,
+                                AbstractType<?>[] clusteringTypes,
                                 RegularAndStaticColumns columns,
                                 EncodingStats stats,
                                 Map<ByteBuffer, AbstractType<?>> typeMap)
@@ -77,7 +78,7 @@ public class SerializationHeader
 
     private SerializationHeader(boolean isForSSTable,
                                 AbstractType<?> keyType,
-                                List<AbstractType<?>> clusteringTypes,
+                                AbstractType<?>[] clusteringTypes,
                                 RegularAndStaticColumns columns,
                                 EncodingStats stats,
                                 Map<ByteBuffer, AbstractType<?>> typeMap,
@@ -191,7 +192,8 @@ public class SerializationHeader
         return keyType;
     }
 
-    public List<AbstractType<?>> clusteringTypes()
+    /** Shared with {@link ClusteringComparator#subtypes()}, or owned when 
deserialized; must not be mutated. */
+    public AbstractType<?>[] clusteringTypes()
     {
         return clusteringTypes;
     }
@@ -313,7 +315,7 @@ public class SerializationHeader
     @Override
     public String toString()
     {
-        return String.format("SerializationHeader[key=%s, cks=%s, columns=%s, 
stats=%s, typeMap=%s]", keyType, clusteringTypes, columns, stats, typeMap);
+        return String.format("SerializationHeader[key=%s, cks=%s, columns=%s, 
stats=%s, typeMap=%s]", keyType, Arrays.toString(clusteringTypes), columns, 
stats, typeMap);
     }
 
     /**
@@ -323,13 +325,13 @@ public class SerializationHeader
     public static class Component extends MetadataComponent
     {
         private final AbstractType<?> keyType;
-        private final List<AbstractType<?>> clusteringTypes;
+        private final AbstractType<?>[] clusteringTypes;
         private final Map<ByteBuffer, AbstractType<?>> staticColumns;
         private final Map<ByteBuffer, AbstractType<?>> regularColumns;
         private final EncodingStats stats;
 
         private Component(AbstractType<?> keyType,
-                          List<AbstractType<?>> clusteringTypes,
+                          AbstractType<?>[] clusteringTypes,
                           Map<ByteBuffer, AbstractType<?>> staticColumns,
                           Map<ByteBuffer, AbstractType<?>> regularColumns,
                           EncodingStats stats)
@@ -397,7 +399,7 @@ public class SerializationHeader
 
             Component that = (Component)o;
             return Objects.equals(this.keyType, that.keyType)
-                && Objects.equals(this.clusteringTypes, that.clusteringTypes)
+                && Arrays.equals(this.clusteringTypes, that.clusteringTypes)
                 && Objects.equals(this.staticColumns, that.staticColumns)
                 && Objects.equals(this.regularColumns, that.regularColumns)
                 && Objects.equals(this.stats, that.stats);
@@ -406,14 +408,14 @@ public class SerializationHeader
         @Override
         public int hashCode()
         {
-            return Objects.hash(keyType, clusteringTypes, staticColumns, 
regularColumns, stats);
+            return 31 * Objects.hash(keyType, staticColumns, regularColumns, 
stats) + Arrays.hashCode(clusteringTypes);
         }
 
         @Override
         public String toString()
         {
             return String.format("SerializationHeader.Component[key=%s, 
cks=%s, statics=%s, regulars=%s, stats=%s]",
-                                 keyType, clusteringTypes, staticColumns, 
regularColumns, stats);
+                                 keyType, Arrays.toString(clusteringTypes), 
staticColumns, regularColumns, stats);
         }
 
         public AbstractType<?> getKeyType()
@@ -421,7 +423,8 @@ public class SerializationHeader
             return keyType;
         }
 
-        public List<AbstractType<?>> getClusteringTypes()
+        /** Shared, as in {@link SerializationHeader#clusteringTypes()}; must 
not be mutated. */
+        public AbstractType<?>[] getClusteringTypes()
         {
             return clusteringTypes;
         }
@@ -560,7 +563,7 @@ public class SerializationHeader
             EncodingStats stats = EncodingStats.serializer.deserialize(in);
 
             AbstractType<?> keyType = metadata.partitionKeyType;
-            List<AbstractType<?>> clusteringTypes = 
metadata.comparator.subtypes();
+            AbstractType<?>[] clusteringTypes = metadata.comparator.subtypes();
 
             Columns statics, regulars;
             if (selection == null)
@@ -602,7 +605,7 @@ public class SerializationHeader
             EncodingStats.serializer.serialize(header.stats, out);
 
             typeSerializer.serialize(header.keyType, out);
-            typeSerializer.serializeList(header.clusteringTypes, out);
+            typeSerializer.serializeArray(header.clusteringTypes, out);
 
             writeColumnsWithTypes(header.staticColumns, out);
             writeColumnsWithTypes(header.regularColumns, out);
@@ -614,7 +617,7 @@ public class SerializationHeader
             EncodingStats stats = EncodingStats.serializer.deserialize(in);
 
             AbstractType<?> keyType = typeSerializer.deserialize(in);
-            List<AbstractType<?>> clusteringTypes = 
typeSerializer.deserializeList(in);
+            AbstractType<?>[] clusteringTypes = 
typeSerializer.deserializeArray(in);
 
             Map<ByteBuffer, AbstractType<?>> staticColumns = 
readColumnsWithType(in);
             Map<ByteBuffer, AbstractType<?>> regularColumns = 
readColumnsWithType(in);
@@ -628,7 +631,7 @@ public class SerializationHeader
             int size = EncodingStats.serializer.serializedSize(header.stats);
 
             size += typeSerializer.serializedSize(header.keyType);
-            size += typeSerializer.serializedListSize(header.clusteringTypes);
+            size += typeSerializer.serializedArraySize(header.clusteringTypes);
 
             size += sizeofColumnsWithTypes(header.staticColumns);
             size += sizeofColumnsWithTypes(header.regularColumns);
@@ -668,4 +671,4 @@ public class SerializationHeader
             return typeMap;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/db/Slice.java 
b/src/java/org/apache/cassandra/db/Slice.java
index 1fc60ba1f7..8838dac820 100644
--- a/src/java/org/apache/cassandra/db/Slice.java
+++ b/src/java/org/apache/cassandra/db/Slice.java
@@ -19,7 +19,6 @@ package org.apache.cassandra.db;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.List;
 import java.util.Objects;
 
 import org.apache.cassandra.db.marshal.AbstractType;
@@ -292,23 +291,23 @@ public class Slice
 
     public static class Serializer
     {
-        public void serialize(Slice slice, DataOutputPlus out, int version, 
List<AbstractType<?>> types) throws IOException
+        public void serialize(Slice slice, DataOutputPlus out, int version, 
AbstractType<?>[] types) throws IOException
         {
             ClusteringBound.serializer.serialize(slice.start, out, version, 
types);
             ClusteringBound.serializer.serialize(slice.end, out, version, 
types);
         }
 
-        public long serializedSize(Slice slice, int version, 
List<AbstractType<?>> types)
+        public long serializedSize(Slice slice, int version, AbstractType<?>[] 
types)
         {
             return ClusteringBound.serializer.serializedSize(slice.start, 
version, types)
                    + ClusteringBound.serializer.serializedSize(slice.end, 
version, types);
         }
 
-        public Slice deserialize(DataInputPlus in, int version, 
List<AbstractType<?>> types) throws IOException
+        public Slice deserialize(DataInputPlus in, int version, 
AbstractType<?>[] types) throws IOException
         {
             ClusteringBound<byte[]> start = (ClusteringBound<byte[]>) 
ClusteringBound.serializer.deserialize(in, version, types);
             ClusteringBound<byte[]> end = (ClusteringBound<byte[]>) 
ClusteringBound.serializer.deserialize(in, version, types);
             return new Slice(start, end);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/db/Slices.java 
b/src/java/org/apache/cassandra/db/Slices.java
index 2f2f6395dc..38811cda02 100644
--- a/src/java/org/apache/cassandra/db/Slices.java
+++ b/src/java/org/apache/cassandra/db/Slices.java
@@ -40,6 +40,8 @@ import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.TableMetadata;
 
+import static 
org.apache.cassandra.db.marshal.AbstractType.EMPTY_ABSTRACT_TYPE_ARRAY;
+
 /**
  * Represents the selection of multiple range of rows within a partition.
  * <p>
@@ -304,9 +306,9 @@ public abstract class Slices implements Iterable<Slice>
             if (size == 0)
                 return;
 
-            List<AbstractType<?>> types = slices == ALL
-                                        ? Collections.emptyList()
-                                        : 
((ArrayBackedSlices)slices).comparator.subtypes();
+            AbstractType<?>[] types = slices == ALL
+                                      ? EMPTY_ABSTRACT_TYPE_ARRAY
+                                      : ((ArrayBackedSlices) 
slices).comparator.subtypes();
 
             for (Slice slice : slices)
                 Slice.serializer.serialize(slice, out, version, types);
@@ -319,9 +321,9 @@ public abstract class Slices implements Iterable<Slice>
             if (slices.size() == 0)
                 return size;
 
-            List<AbstractType<?>> types = slices instanceof SelectAllSlices
-                                        ? Collections.emptyList()
-                                        : 
((ArrayBackedSlices)slices).comparator.subtypes();
+            AbstractType<?>[] types = slices instanceof SelectAllSlices
+                                      ? EMPTY_ABSTRACT_TYPE_ARRAY
+                                      : ((ArrayBackedSlices) 
slices).comparator.subtypes();
 
             for (Slice slice : slices)
                 size += Slice.serializer.serializedSize(slice, version, types);
@@ -864,4 +866,4 @@ public abstract class Slices implements Iterable<Slice>
             return "";
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/db/compaction/CursorCompactor.java 
b/src/java/org/apache/cassandra/db/compaction/CursorCompactor.java
index d49931d6b3..9d0deece00 100644
--- a/src/java/org/apache/cassandra/db/compaction/CursorCompactor.java
+++ b/src/java/org/apache/cassandra/db/compaction/CursorCompactor.java
@@ -524,7 +524,7 @@ public class CursorCompactor extends CompactionInfo.Holder
         purger = new Purger(type, controller);
 
         lastWrittenPartition = new 
PartitionDescriptor(metadata.partitioner.createReusableKey(0));
-        lastWrittenUnfiltered = new 
UnfilteredDescriptor(metadata.comparator.subtypes().toArray(AbstractType[]::new));
+        lastWrittenUnfiltered = new 
UnfilteredDescriptor(metadata.comparator.subtypes());
         // A steal moves a descriptor between cursors, and to the write-side 
instance built from
         // the table comparator above. Each parses a clustering with its own 
clusteringTypes, so
         // all of them must parse identically.
diff --git a/src/java/org/apache/cassandra/db/compaction/StatefulCursor.java 
b/src/java/org/apache/cassandra/db/compaction/StatefulCursor.java
index aeea5da967..83042ffe0a 100644
--- a/src/java/org/apache/cassandra/db/compaction/StatefulCursor.java
+++ b/src/java/org/apache/cassandra/db/compaction/StatefulCursor.java
@@ -28,7 +28,6 @@ import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.ReusableLivenessInfo;
 import org.apache.cassandra.db.UnfilteredValidation;
-import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.rows.ReusableCellLivenessInfo;
 import org.apache.cassandra.io.sstable.PartitionDescriptor;
 import org.apache.cassandra.io.sstable.SSTableCursorReader;
@@ -82,7 +81,7 @@ class StatefulCursor extends SSTableCursorReader
         // true on SSTableCursorReader for exactly this reason; no override 
needed here.
         currPartition = new 
PartitionDescriptor(reader.getPartitioner().createReusableKey(0));
         prevPartition = new 
PartitionDescriptor(reader.getPartitioner().createReusableKey(0));
-        unfiltered = new 
UnfilteredDescriptor(reader.header.clusteringTypes().toArray(AbstractType[]::new));
+        unfiltered = new UnfilteredDescriptor(reader.header.clusteringTypes());
     }
 
     public int readPartitionHeader()
diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java 
b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
index 257a64f931..1784b10eb2 100644
--- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java
+++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
@@ -67,6 +67,8 @@ public abstract class AbstractType<T> implements 
Comparator<ByteBuffer>, Assignm
 {
     private final static int VARIABLE_LENGTH = -1;
 
+    public static final AbstractType<?>[] EMPTY_ABSTRACT_TYPE_ARRAY = new 
AbstractType<?>[0];
+
     public final Comparator<ByteBuffer> reverseComparator;
 
     public enum ComparisonType
diff --git a/src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java 
b/src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java
index 07f96367f6..aa5b1dfd73 100644
--- a/src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java
+++ b/src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java
@@ -20,7 +20,6 @@ package org.apache.cassandra.io.sstable;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.List;
 
 import org.apache.cassandra.db.Clustering;
 import org.apache.cassandra.db.ClusteringBound;
@@ -163,7 +162,7 @@ public class ClusteringDescriptor extends 
ResizableByteBuffer
         return (clusteringKindEncoded == 
EXCL_END_INCL_START_BOUNDARY_CLUSTERING_KIND || clusteringKindEncoded == 
INCL_END_EXCL_START_BOUNDARY_CLUSTERING_KIND);
     }
 
-    public final ClusteringPrefix<?> toClusteringPrefix(List<AbstractType<?>> 
clusteringTypesList) {
+    public final ClusteringPrefix<?> toClusteringPrefix(AbstractType<?>[] 
clusteringTypesList) {
         if (clusteringKindEncoded == ROW_CLUSTERING_KIND) {
             return Clustering.serializer.deserialize(clusteringBuffer(), 0, 
clusteringTypesList);
         }
diff --git a/src/java/org/apache/cassandra/io/sstable/IndexInfo.java 
b/src/java/org/apache/cassandra/io/sstable/IndexInfo.java
index 607d876c0f..2be23943cc 100644
--- a/src/java/org/apache/cassandra/io/sstable/IndexInfo.java
+++ b/src/java/org/apache/cassandra/io/sstable/IndexInfo.java
@@ -19,7 +19,6 @@
 package org.apache.cassandra.io.sstable;
 
 import java.io.IOException;
-import java.util.List;
 
 import org.apache.cassandra.db.ClusteringPrefix;
 import org.apache.cassandra.db.DeletionTime;
@@ -108,9 +107,9 @@ public class IndexInfo
         public static final long WIDTH_BASE = 64 * 1024;
 
         private final Version version;
-        private final List<AbstractType<?>> clusteringTypes;
+        private final AbstractType<?>[] clusteringTypes;
 
-        public Serializer(Version version, List<AbstractType<?>> 
clusteringTypes)
+        public Serializer(Version version, AbstractType<?>[] clusteringTypes)
         {
             this.version = version;
             this.clusteringTypes = clusteringTypes;
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java 
b/src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java
index 49c61e2238..8dcafc4e58 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java
@@ -68,7 +68,6 @@ public class SSTableCursorWriter implements AutoCloseable
     private static final ColumnMetadata[] EMPTY_COL_META = new 
ColumnMetadata[0];
     private final SortedTableWriter<?,?> ssTableWriter;
     private final SequentialWriter dataWriter;
-    private final SortedTableWriter.AbstractIndexWriter indexWriter;
     private final DeletionTime.Serializer deletionTimeSerializer;
     private final MetadataCollector metadataCollector;
     private final SerializationHeader serializationHeader;
@@ -160,7 +159,6 @@ public class SSTableCursorWriter implements AutoCloseable
     {
         this.ssTableWriter = ssTableWriter;
         this.dataWriter = dataWriter;
-        this.indexWriter = indexWriter;
         this.deletionTimeSerializer = DeletionTime.getSerializer(desc.version);
         this.metadataCollector = metadataCollector;
         this.serializationHeader = serializationHeader;
diff --git 
a/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java 
b/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java
index f74412cee4..fc6bf1ee30 100644
--- a/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java
+++ b/src/java/org/apache/cassandra/io/sstable/metadata/MetadataCollector.java
@@ -18,7 +18,6 @@
 package org.apache.cassandra.io.sstable.metadata;
 
 import java.nio.ByteBuffer;
-import java.util.Collections;
 import java.util.EnumMap;
 import java.util.Map;
 import java.util.UUID;
@@ -59,6 +58,8 @@ import org.apache.cassandra.utils.TimeUUID;
 import 
org.apache.cassandra.utils.streamhist.StreamingTombstoneHistogramBuilder;
 import org.apache.cassandra.utils.streamhist.TombstoneHistogram;
 
+import static 
org.apache.cassandra.db.marshal.AbstractType.EMPTY_ABSTRACT_TYPE_ARRAY;
+
 public class MetadataCollector implements PartitionStatisticsCollector
 {
     public static final double NO_COMPRESSION_RATIO = -1.0;
@@ -97,7 +98,7 @@ public class MetadataCollector implements 
PartitionStatisticsCollector
                                  NO_COMPRESSION_RATIO,
                                  defaultTombstoneDropTimeHistogram(),
                                  0,
-                                 Collections.emptyList(),
+                                 EMPTY_ABSTRACT_TYPE_ARRAY,
                                  Slice.ALL,
                                  true,
                                  ActiveRepairService.UNREPAIRED_SSTABLE,
@@ -170,7 +171,7 @@ public class MetadataCollector implements 
PartitionStatisticsCollector
     {
         this.comparator = comparator;
         this.originatingHostId = originatingHostId;
-        AbstractType<?>[] clusteringTypes = 
comparator.subtypes().toArray(AbstractType[]::new);
+        AbstractType<?>[] clusteringTypes = comparator.subtypes();
         this.minClusteringDescriptor = new 
ClusteringDescriptor(clusteringTypes).resetMaxStart();
         this.maxClusteringDescriptor = new 
ClusteringDescriptor(clusteringTypes).resetMinEnd();
     }
@@ -604,4 +605,4 @@ public class MetadataCollector implements 
PartitionStatisticsCollector
             return isSet ? max : defaultMax;
         }
     }
-}
\ No newline at end of file
+}
diff --git 
a/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java 
b/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java
index 3ec1a208c5..5f1c6cac6e 100644
--- a/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java
+++ b/src/java/org/apache/cassandra/io/sstable/metadata/StatsMetadata.java
@@ -19,7 +19,6 @@ package org.apache.cassandra.io.sstable.metadata;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.List;
 import java.util.UUID;
 
 import org.apache.commons.lang3.ArrayUtils;
@@ -83,7 +82,7 @@ public class StatsMetadata extends MetadataComponent
     public final EncodingStats encodingStats;
 
     // Used to serialize min/max clustering. Can be null if the metadata was 
deserialized from a legacy version
-    private final List<AbstractType<?>> clusteringTypes;
+    private final AbstractType<?>[] clusteringTypes;
 
     /**
      * This boolean is used as an approximation of whether a given key can be 
guaranteed not to have partition
@@ -111,7 +110,7 @@ public class StatsMetadata extends MetadataComponent
                          double compressionRatio,
                          TombstoneHistogram estimatedTombstoneDropTime,
                          int sstableLevel,
-                         List<AbstractType<?>> clusteringTypes,
+                         AbstractType<?>[] clusteringTypes,
                          Slice coveredClustering,
                          boolean hasLegacyCounterShards,
                          long repairedAt,
@@ -391,7 +390,7 @@ public class StatsMetadata extends MetadataComponent
 
         private int improvedMinMaxSize(Version version, StatsMetadata 
component, int size)
         {
-            size += 
typeSerializer.serializedListSize(component.clusteringTypes);
+            size += 
typeSerializer.serializedArraySize(component.clusteringTypes);
             size += 
Slice.serializer.serializedSize(component.coveredClustering,
                                                     
version.correspondingMessagingVersion(),
                                                     component.clusteringTypes);
@@ -514,7 +513,7 @@ public class StatsMetadata extends MetadataComponent
         private void serializeImprovedMinMax(Version version, StatsMetadata 
component, DataOutputPlus out) throws IOException
         {
             assert component.clusteringTypes != null;
-            typeSerializer.serializeList(component.clusteringTypes, out);
+            typeSerializer.serializeArray(component.clusteringTypes, out);
             Slice.serializer.serialize(component.coveredClustering,
                                        out,
                                        version.correspondingMessagingVersion(),
@@ -573,7 +572,7 @@ public class StatsMetadata extends MetadataComponent
             int sstableLevel = in.readInt();
             long repairedAt = in.readLong();
 
-            List<AbstractType<?>> clusteringTypes = null;
+            AbstractType<?>[] clusteringTypes = null;
             Slice coveredClustering = Slice.ALL;
             if (version.hasLegacyMinMax())
             {
@@ -596,7 +595,7 @@ public class StatsMetadata extends MetadataComponent
             else if (version.hasImprovedMinMax())
             {
                 // improvedMinMax will be in this place when legacyMinMax is 
removed
-                clusteringTypes = typeSerializer.deserializeList(in);
+                clusteringTypes = typeSerializer.deserializeArray(in);
                 coveredClustering = Slice.serializer.deserialize(in, 
version.correspondingMessagingVersion(), clusteringTypes);
             }
 
@@ -636,7 +635,7 @@ public class StatsMetadata extends MetadataComponent
             if (version.hasImprovedMinMax() && version.hasLegacyMinMax())
             {
                 // improvedMinMax will be in this place until legacyMinMax is 
removed
-                clusteringTypes = typeSerializer.deserializeList(in);
+                clusteringTypes = typeSerializer.deserializeArray(in);
                 coveredClustering = Slice.serializer.deserialize(in, 
version.correspondingMessagingVersion(), clusteringTypes);
             }
 
diff --git a/src/java/org/apache/cassandra/schema/TableMetadata.java 
b/src/java/org/apache/cassandra/schema/TableMetadata.java
index 4f9fe1320a..5dae17eda9 100644
--- a/src/java/org/apache/cassandra/schema/TableMetadata.java
+++ b/src/java/org/apache/cassandra/schema/TableMetadata.java
@@ -91,7 +91,6 @@ import static accord.utils.Invariants.require;
 import static com.google.common.collect.Iterables.any;
 import static com.google.common.collect.Iterables.transform;
 import static java.lang.String.format;
-import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 import static 
org.apache.cassandra.db.Directories.SECONDARY_INDEX_NAME_SEPARATOR;
 import static 
org.apache.cassandra.db.Directories.TABLE_DIRECTORY_NAME_SEPARATOR;
@@ -255,8 +254,13 @@ public class TableMetadata implements SchemaElement
             columnsById[column.uniqueId] = column;
         for (ColumnMetadata column : partitionKeyColumns)
             columnsById[column.uniqueId] = column;
-        for (ColumnMetadata column : clusteringColumns)
+        AbstractType<?>[] clusteringTypes = new 
AbstractType<?>[clusteringColumns.size()];
+        for (int i = 0; i < clusteringColumns.size(); i++)
+        {
+            ColumnMetadata column = clusteringColumns.get(i);
             columnsById[column.uniqueId] = column;
+            clusteringTypes[i] = column.type;
+        }
         columns = ImmutableMap.copyOf(builder.columns);
 
         indexes = builder.indexes;
@@ -266,7 +270,7 @@ public class TableMetadata implements SchemaElement
                          ? partitionKeyColumns.get(0).type
                          : 
CompositeType.getInstance(transform(partitionKeyColumns, t -> t.type));
 
-        comparator = new ClusteringComparator(transform(clusteringColumns, c 
-> c.type));
+        comparator = new ClusteringComparator(clusteringTypes);
 
         resource = DataResource.table(keyspace, name);
         if (builder.isOffline)
@@ -750,7 +754,10 @@ public class TableMetadata implements SchemaElement
 
     public ClusteringComparator partitionKeyAsClusteringComparator()
     {
-        return new ClusteringComparator(partitionKeyColumns.stream().map(c -> 
c.type).collect(toList()));
+        AbstractType<?>[] clusteringTypes = new 
AbstractType<?>[partitionKeyColumns.size()];
+        for (int i = 0; i < clusteringTypes.length; i++)
+            clusteringTypes[i] = partitionKeyColumns.get(i).type;
+        return new ClusteringComparator(clusteringTypes);
     }
 
     /**
diff --git 
a/src/java/org/apache/cassandra/serializers/AbstractTypeSerializer.java 
b/src/java/org/apache/cassandra/serializers/AbstractTypeSerializer.java
index 1be4d61f68..387fae7906 100644
--- a/src/java/org/apache/cassandra/serializers/AbstractTypeSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/AbstractTypeSerializer.java
@@ -20,8 +20,6 @@ package org.apache.cassandra.serializers;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.List;
 
 import org.apache.cassandra.db.TypeSizes;
 import org.apache.cassandra.db.marshal.AbstractType;
@@ -38,11 +36,12 @@ public class AbstractTypeSerializer
         
ByteBufferUtil.writeWithVIntLength(UTF8Type.instance.decompose(type.toString()),
 out);
     }
 
-    public void serializeList(List<AbstractType<?>> types, DataOutputPlus out) 
throws IOException
+    public void serializeArray(AbstractType<?>[] types, DataOutputPlus out) 
throws IOException
     {
-        out.writeUnsignedVInt32(types.size());
-        for (AbstractType<?> type : types)
-            serialize(type, out);
+        int size = types.length;
+        out.writeUnsignedVInt32(size);
+        for (int i = 0; i < size; i++)
+            serialize(types[i], out);
     }
 
     public AbstractType<?> deserialize(DataInputPlus in) throws IOException
@@ -51,12 +50,12 @@ public class AbstractTypeSerializer
         return TypeParser.parse(UTF8Type.instance.compose(raw));
     }
 
-    public List<AbstractType<?>> deserializeList(DataInputPlus in) throws 
IOException
+    public AbstractType<?>[] deserializeArray(DataInputPlus in) throws 
IOException
     {
         int size = (int) in.readUnsignedVInt();
-        List<AbstractType<?>> types = new ArrayList<>(size);
+        AbstractType<?>[] types = new AbstractType<?>[size];
         for (int i = 0; i < size; i++)
-            types.add(deserialize(in));
+            types[i] = deserialize(in);
         return types;
     }
 
@@ -65,11 +64,11 @@ public class AbstractTypeSerializer
         return 
ByteBufferUtil.serializedSizeWithVIntLength(UTF8Type.instance.decompose(type.toString()));
     }
 
-    public long serializedListSize(List<AbstractType<?>> types)
+    public long serializedArraySize(AbstractType<?>[] types)
     {
-        long size = TypeSizes.sizeofUnsignedVInt(types.size());
-        for (AbstractType<?> type : types)
-            size += serializedSize(type);
+        long size = TypeSizes.sizeofUnsignedVInt(types.length);
+        for (int i = 0; i < types.length; i++)
+            size += serializedSize(types[i]);
         return size;
     }
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/service/pager/PagingState.java 
b/src/java/org/apache/cassandra/service/pager/PagingState.java
index ef19275501..5abca604e0 100644
--- a/src/java/org/apache/cassandra/service/pager/PagingState.java
+++ b/src/java/org/apache/cassandra/service/pager/PagingState.java
@@ -19,7 +19,6 @@ package org.apache.cassandra.service.pager;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
@@ -369,7 +368,7 @@ public class PagingState
             this.protocolVersion = protocolVersion;
         }
 
-        private static List<AbstractType<?>> makeClusteringTypes(TableMetadata 
metadata)
+        private static AbstractType<?>[] makeClusteringTypes(TableMetadata 
metadata)
         {
             // This is the types that will be used when serializing the 
clustering in the paging state. We can't really use the actual clustering
             // types however because we can't guarantee that there won't be a 
schema change between when we send the paging state and get it back,
@@ -377,9 +376,8 @@ public class PagingState
             // (say timestamp -> blob). So we simply use a list of BytesTypes 
(for both reading and writting), which may be slightly inefficient
             // for fixed-width types, but avoid any risk during schema changes.
             int size = metadata.clusteringColumns().size();
-            List<AbstractType<?>> l = new ArrayList<>(size);
-            for (int i = 0; i < size; i++)
-                l.add(BytesType.instance);
+            AbstractType<?>[] l = new AbstractType<?>[size];
+            Arrays.fill(l, BytesType.instance);
             return l;
         }
 
diff --git a/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java 
b/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java
index 2f526a5c23..3d56b723b9 100644
--- a/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java
+++ b/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java
@@ -21,8 +21,8 @@ import java.io.IOException;
 import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.nio.ByteBuffer;
+import java.util.Arrays;
 import java.util.Comparator;
-import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -403,7 +403,7 @@ public class SSTableMetadataViewer
         {
             EncodingStats encodingStats = header.getEncodingStats();
             AbstractType<?> keyType = header.getKeyType();
-            List<AbstractType<?>> clusteringTypes = 
header.getClusteringTypes();
+            AbstractType<?>[] clusteringTypes = header.getClusteringTypes();
             Map<ByteBuffer, AbstractType<?>> staticColumns = 
header.getStaticColumns();
             Map<String, String> statics = staticColumns.entrySet().stream()
                     .collect(Collectors.toMap(e -> 
UTF8Type.instance.getString(e.getKey()),
@@ -420,7 +420,7 @@ public class SSTableMetadataViewer
             field("EncodingStats minTimestamp", 
toDateString(encodingStats.minTimestamp, tsUnit),
                     Long.toString(encodingStats.minTimestamp));
             field("KeyType", keyType.toString());
-            field("ClusteringTypes", clusteringTypes.toString());
+            field("ClusteringTypes", Arrays.toString(clusteringTypes));
             field("StaticColumns", FBUtilities.toString(statics));
             field("RegularColumns", FBUtilities.toString(regulars));
             if (stats != null)
diff --git a/src/java/org/apache/cassandra/tools/Util.java 
b/src/java/org/apache/cassandra/tools/Util.java
index 4c4557624f..20845a7e54 100644
--- a/src/java/org/apache/cassandra/tools/Util.java
+++ b/src/java/org/apache/cassandra/tools/Util.java
@@ -37,6 +37,7 @@ import com.google.common.collect.Lists;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.SerializationHeader;
+import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.exceptions.ConfigurationException;
@@ -331,9 +332,10 @@ public final class Util
                     builder.addRegularColumn(ident, entry.getValue());
                 });
         builder.addPartitionKeyColumn("PartitionKey", header.getKeyType());
-        for (int i = 0; i < header.getClusteringTypes().size(); i++)
+        AbstractType<?>[] clusteringTypes = header.getClusteringTypes();
+        for (int i = 0; i < clusteringTypes.length; i++)
         {
-            builder.addClusteringColumn("clustering" + (i > 0 ? i : ""), 
header.getClusteringTypes().get(i));
+            builder.addClusteringColumn("clustering" + (i > 0 ? i : ""), 
clusteringTypes[i]);
         }
         if (SecondaryIndexManager.isIndexColumnFamily(desc.cfname))
         {
@@ -346,4 +348,4 @@ public final class Util
         }
         return builder.build();
     }
-}
\ No newline at end of file
+}
diff --git a/test/bin/jmh b/test/bin/jmh
index 53cf166de9..9bb6310151 100755
--- a/test/bin/jmh
+++ b/test/bin/jmh
@@ -136,6 +136,13 @@ CLASSPATH="$CLASSPATH:$CASSANDRA_HOME/test/conf/"
 CLASSPATH="$CLASSPATH:$CASSANDRA_HOME/build/test/classes/"
 CLASSPATH="$CLASSPATH:$CASSANDRA_HOME/build/test/lib/jars/*"
 
-exec $NUMACTL "$JAVA" -Xmx8g -XX:+UseParallelGC -cp "$CLASSPATH" 
org.openjdk.jmh.Main -jvmArgs="-Djmh.shutdownTimeout=120 $cassandra_parms 
$JVM_OPTS" "$@"
+if [ -n "${JMH_CLASSPATH_PREFIX:-}" ] ; then
+    CLASSPATH="$JMH_CLASSPATH_PREFIX:$CLASSPATH"
+fi
+
+JMH_OPTS="${JMH_OPTS:-}"
+
+# $JMH_OPTS is deliberately unquoted: it carries several whitespace-separated 
JMH flags, e.g. JMH_OPTS="-gc true -prof gc".
+exec $NUMACTL "$JAVA" -Xmx8g -XX:+UseParallelGC -cp "$CLASSPATH" 
org.openjdk.jmh.Main -jvmArgs="-Djmh.shutdownTimeout=120 $cassandra_parms 
$JVM_OPTS" $JMH_OPTS "$@"
 
 # vi:ai sw=4 ts=4 tw=0 et
diff --git 
a/test/harry/main/org/apache/cassandra/harry/model/BytesPartitionState.java 
b/test/harry/main/org/apache/cassandra/harry/model/BytesPartitionState.java
index b64d9613bb..e7adc26ae4 100644
--- a/test/harry/main/org/apache/cassandra/harry/model/BytesPartitionState.java
+++ b/test/harry/main/org/apache/cassandra/harry/model/BytesPartitionState.java
@@ -572,7 +572,7 @@ public class BytesPartitionState
             selectionOrder = symbolListBuilder.buildAndClear();
             regularAndStaticColumns = 
symbolListBuilder.addAll(staticColumns).addAll(regularColumns).buildAndClear();
 
-            clusteringComparator = new 
ClusteringComparator(clusteringColumns.stream().map(Symbol::rawType).collect(Collectors.toList()));
+            clusteringComparator = new 
ClusteringComparator(clusteringColumns.stream().map(Symbol::rawType).toArray(AbstractType<?>[]::new));
 
             List<Comparator<Object>> pkComparators = new 
ArrayList<>(partitionColumns.size());
             for (var p : partitionColumns)
diff --git 
a/test/microbench/org/apache/cassandra/test/microbench/ClusteringPrefixDeserializerBench.java
 
b/test/microbench/org/apache/cassandra/test/microbench/ClusteringPrefixDeserializerBench.java
new file mode 100644
index 0000000000..d569b7c760
--- /dev/null
+++ 
b/test/microbench/org/apache/cassandra/test/microbench/ClusteringPrefixDeserializerBench.java
@@ -0,0 +1,169 @@
+/*
+ * 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.cassandra.test.microbench;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.ClusteringBoundOrBoundary;
+import org.apache.cassandra.db.ClusteringComparator;
+import org.apache.cassandra.db.ClusteringPrefix;
+import org.apache.cassandra.db.SerializationHeader;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.rows.UnfilteredSerializer;
+import org.apache.cassandra.io.sstable.format.SSTableFormat;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+/**
+ * Measures {@link ClusteringPrefix.Deserializer} while rotating over 1 to 3 
header sources: a two-column memtable
+ * header, a one-column memtable header, and a two-column header round-tripped 
through the SSTable metadata. This is
+ * what shapes the profile of the clustering-type lookup in {@code 
deserializeOne} (CASSANDRA-21587). Every operation
+ * reads one non-empty component, so all sources do the same work.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(value = 3,
+      jvmArgsAppend = { "-Xms1G", "-Xmx1G", "-Djmh.executor=CUSTOM",
+                         
"-Djmh.executor.class=org.apache.cassandra.test.microbench.FastThreadExecutor" 
})
+@Threads(1)
+@State(Scope.Thread)
+public class ClusteringPrefixDeserializerBench
+{
+    @Param({ "1", "2", "3" })
+    public int headerSources;
+
+    private DeserializerState[] states;
+    private int index;
+
+    @Setup
+    public void setup() throws Exception
+    {
+        DatabaseDescriptor.daemonInitialization();
+
+        TableMetadata oneColumn = schema("one_column", 1);
+        TableMetadata twoColumns = schema("two_columns", 2);
+        SerializationHeader oneColumnHeader = 
SerializationHeader.makeWithoutStats(oneColumn);
+        SerializationHeader twoColumnHeader = 
SerializationHeader.makeWithoutStats(twoColumns);
+        SerializationHeader sstableHeader = deserializeHeader(twoColumnHeader, 
twoColumns);
+
+        DeserializerState twoColumn = state(twoColumns.comparator, 
twoColumnHeader);
+        DeserializerState singleColumn = state(oneColumn.comparator, 
oneColumnHeader);
+        DeserializerState sstable = state(twoColumns.comparator, 
sstableHeader);
+
+        switch (headerSources)
+        {
+            case 1:
+                states = new DeserializerState[] { twoColumn };
+                break;
+            case 2:
+                states = new DeserializerState[] { twoColumn, singleColumn };
+                break;
+            case 3:
+                states = new DeserializerState[] { twoColumn, singleColumn, 
sstable };
+                break;
+            default:
+                throw new AssertionError("Unsupported number of header 
sources: " + headerSources);
+        }
+    }
+
+    /**
+     * Rotates through {@link #states} with a conditional reset rather than 
{@code index++ % states.length}: a modulo
+     * by a non-constant divisor costs an integer division (tens of cycles) on 
an operation that only takes a few tens
+     * of nanoseconds, and {@code index++} eventually overflows negative. The 
reset keeps the exact 1/2/3 mix of header
+     * sources, which is what shapes the call-site profile being measured.
+     */
+    @Benchmark
+    public ClusteringBoundOrBoundary<byte[]> deserializeBound() throws 
IOException
+    {
+        int i = index;
+        DeserializerState state = states[i];
+        index = ++i == states.length ? 0 : i;
+        state.in.buffer().rewind();
+        state.deserializer.prepare(UnfilteredSerializer.IS_MARKER, 0);
+        return state.deserializer.deserializeNextBound();
+    }
+
+    private static TableMetadata schema(String name, int clusteringColumns)
+    {
+        TableMetadata.Builder builder = 
TableMetadata.builder("ClusteringPrefixDeserializerBench", name)
+                                                     
.addPartitionKeyColumn("k", Int32Type.instance);
+        for (int i = 0; i < clusteringColumns; i++)
+            builder.addClusteringColumn("c" + i, Int32Type.instance);
+        return builder.build();
+    }
+
+    private static SerializationHeader deserializeHeader(SerializationHeader 
header, TableMetadata metadata)
+    throws Exception
+    {
+        SSTableFormat<?, ?> format = 
DatabaseDescriptor.getSelectedSSTableFormat();
+        try (DataOutputBuffer out = new DataOutputBuffer())
+        {
+            
SerializationHeader.serializer.serialize(format.getLatestVersion(), 
header.toComponent(), out);
+            SerializationHeader.Component component = 
SerializationHeader.serializer.deserialize(format.getLatestVersion(),
+                                                                               
                    new DataInputBuffer(out.buffer(), true));
+            return component.toHeader(metadata);
+        }
+    }
+
+    private static DeserializerState state(ClusteringComparator comparator, 
SerializationHeader header)
+    throws IOException
+    {
+        try (DataOutputBuffer out = new DataOutputBuffer())
+        {
+            out.writeByte(ClusteringPrefix.Kind.INCL_START_BOUND.ordinal());
+            out.writeShort(1);
+            out.writeUnsignedVInt32(0);
+            header.clusteringTypes()[0].writeValue(ByteBufferUtil.bytes(0), 
out);
+
+            DataInputBuffer in = new DataInputBuffer(out.buffer(), true);
+            return new DeserializerState(in, new 
ClusteringPrefix.Deserializer(comparator, in, header));
+        }
+    }
+
+    private static class DeserializerState
+    {
+        final DataInputBuffer in;
+        final ClusteringPrefix.Deserializer deserializer;
+
+        private DeserializerState(DataInputBuffer in, 
ClusteringPrefix.Deserializer deserializer)
+        {
+            this.in = in;
+            this.deserializer = deserializer;
+        }
+    }
+}
diff --git 
a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableCursorPipeUtil.java
 
b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableCursorPipeUtil.java
index 7f2c31828e..9c2611af7e 100644
--- 
a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableCursorPipeUtil.java
+++ 
b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableCursorPipeUtil.java
@@ -21,7 +21,6 @@ package org.apache.cassandra.test.microbench.sstable;
 import java.io.IOException;
 
 import org.apache.cassandra.db.DeletionTime;
-import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.rows.Cell;
 import org.apache.cassandra.db.rows.ReusableCellLivenessInfo;
 import org.apache.cassandra.io.sstable.PartitionDescriptor;
@@ -46,7 +45,7 @@ public class SSTableCursorPipeUtil
     public static void copySSTable(SSTableCursorReader reader, 
SSTableCursorWriter writer) throws Throwable
     {
         PartitionDescriptor pHeader = new 
PartitionDescriptor(reader.ssTableReader().getPartitioner().createReusableKey(0));
-        UnfilteredDescriptor unfilteredDescriptor = new 
UnfilteredDescriptor(reader.ssTableReader().header.clusteringTypes().toArray(AbstractType[]::new));
+        UnfilteredDescriptor unfilteredDescriptor = new 
UnfilteredDescriptor(reader.ssTableReader().header.clusteringTypes());
         int readerState = PARTITION_START;
         boolean first = true;
 
diff --git 
a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileCursorBench.java
 
b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileCursorBench.java
index 5e73ed604b..80c458ab35 100644
--- 
a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileCursorBench.java
+++ 
b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileCursorBench.java
@@ -38,7 +38,6 @@ import org.openjdk.jmh.annotations.TearDown;
 import org.openjdk.jmh.annotations.Warmup;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.PartitionDescriptor;
 import org.apache.cassandra.io.sstable.SSTableCursorReader;
@@ -112,7 +111,7 @@ public class SSTableReadingFileCursorBench
         Arrays.fill(counters, 0);
         int state = PARTITION_START;
         PartitionDescriptor pHeader = new 
PartitionDescriptor(cursor.ssTableReader().getPartitioner().createReusableKey(0));
-        UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(cursor.ssTableReader().header.clusteringTypes().toArray(AbstractType[]::new));
+        UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(cursor.ssTableReader().header.clusteringTypes());
         while (state != DONE) {
             state = cursor.readPartitionHeader(pHeader);
             counters[0]++;
diff --git a/test/unit/org/apache/cassandra/db/ClusteringComparatorTest.java 
b/test/unit/org/apache/cassandra/db/ClusteringComparatorTest.java
index 586d203c81..7dc0fd25c8 100644
--- a/test/unit/org/apache/cassandra/db/ClusteringComparatorTest.java
+++ b/test/unit/org/apache/cassandra/db/ClusteringComparatorTest.java
@@ -20,9 +20,9 @@ package org.apache.cassandra.db;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.List;
 import java.util.concurrent.ThreadLocalRandom;
 
+import org.junit.Ignore;
 import org.junit.Test;
 
 import org.apache.cassandra.db.marshal.AbstractType;
@@ -32,13 +32,67 @@ import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertSame;
 
 public class ClusteringComparatorTest
 {
+    @Test
+    public void clusteringTypesAreSharedNotCopied()
+    {
+        AbstractType<?>[] types = { LongType.instance, Int32Type.instance };
+        ClusteringComparator comparator = new ClusteringComparator(types);
+
+        assertSame(types, comparator.subtypes());
+        assertSame(comparator.subtypes(), comparator.subtypes());
+        assertSame(LongType.instance, comparator.subtype(0));
+
+        assertEquals(0, new ClusteringComparator().subtypes().length);
+        assertSame(new ClusteringComparator().subtypes(), new 
ClusteringComparator().subtypes());
+    }
+
+    @Ignore("Enable this test if mutation detection is enabled in 
ClusteringComparator")
+    @Test
+    public void mutationOfTheSharedClusteringTypesIsDetected()
+    {
+        AbstractType<?>[] types = { LongType.instance, Int32Type.instance };
+        ClusteringComparator comparator = new ClusteringComparator(types);
+
+        types[0] = Int32Type.instance;
+        try
+        {
+            comparator.subtypes();
+            throw new RuntimeException("mutating the clustering types of a 
comparator must be detected");
+        }
+        catch (AssertionError e)
+        {
+            // expected
+        }
+
+        types[0] = LongType.instance;
+        assertSame(types, comparator.subtypes());
+    }
+
+    @Test
+    public void equalsAndHashCodeAreContentBased()
+    {
+        ClusteringComparator a = new ClusteringComparator(LongType.instance, 
Int32Type.instance);
+        ClusteringComparator b = new ClusteringComparator(LongType.instance, 
Int32Type.instance);
+        ClusteringComparator different = new 
ClusteringComparator(LongType.instance);
+
+        assertEquals(a, b);
+        assertEquals(a.hashCode(), b.hashCode());
+        assertNotEquals(a, different);
+
+        ClusteringComparator empty1 = new ClusteringComparator();
+        ClusteringComparator empty2 = new ClusteringComparator();
+        assertEquals(empty1, empty2);
+        assertEquals(empty1.hashCode(), empty2.hashCode());
+    }
+
     @Test
     public void compareLong()
     {
-        Iterable<AbstractType<?>> types;
         ClusteringComparator comparator = new 
ClusteringComparator(LongType.instance);
         for (int i=0;i<1000; i++) {
             long l1 = ThreadLocalRandom.current().nextLong();
@@ -103,7 +157,7 @@ public class ClusteringComparatorTest
     {
         Clustering<ByteBuffer> clustering = 
Clustering.make(ByteBufferUtil.bytes(v1));
         DataOutputBuffer out = new DataOutputBuffer();
-        Clustering.serializer.serialize(clustering, out, 0, List.of(types));
+        Clustering.serializer.serialize(clustering, out, 0, types);
         return out.asNewBuffer();
     }
 
@@ -111,7 +165,7 @@ public class ClusteringComparatorTest
     {
         Clustering<ByteBuffer> clustering = 
Clustering.make(ByteBufferUtil.bytes(v1));
         DataOutputBuffer out = new DataOutputBuffer();
-        Clustering.serializer.serialize(clustering, out, 0, List.of(types));
+        Clustering.serializer.serialize(clustering, out, 0, types);
         return out.asNewBuffer();
     }
-}
\ No newline at end of file
+}
diff --git a/test/unit/org/apache/cassandra/db/SerializationHeaderTest.java 
b/test/unit/org/apache/cassandra/db/SerializationHeaderTest.java
index 0993606739..fef3f66a13 100644
--- a/test/unit/org/apache/cassandra/db/SerializationHeaderTest.java
+++ b/test/unit/org/apache/cassandra/db/SerializationHeaderTest.java
@@ -35,6 +35,7 @@ import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.compaction.OperationType;
 import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
+import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.db.marshal.ListType;
 import org.apache.cassandra.db.marshal.LongType;
@@ -52,6 +53,8 @@ import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.sstable.format.SSTableWriter;
 import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.File;
 import org.apache.cassandra.io.util.FileUtils;
 import org.apache.cassandra.schema.ColumnMetadata;
@@ -67,6 +70,72 @@ public class SerializationHeaderTest
         DatabaseDescriptor.daemonInitialization();
     }
 
+    /** The schema, the header and its component all share one clustering-type 
array (no copy). */
+    @Test
+    public void testClusteringTypesSharingContract()
+    {
+        TableMetadata schema = TableMetadata.builder(KEYSPACE, 
"testClusteringTypesSharingContract")
+                                            .addPartitionKeyColumn("k", 
Int32Type.instance)
+                                            .addClusteringColumn("c", 
Int32Type.instance)
+                                            .build();
+
+        AbstractType<?>[] schemaTypes = schema.comparator.subtypes();
+        SerializationHeader header = 
SerializationHeader.makeWithoutStats(schema);
+        Assert.assertSame(schemaTypes, header.clusteringTypes());
+        Assert.assertSame(schemaTypes, 
header.toComponent().getClusteringTypes());
+        Assert.assertArrayEquals(new AbstractType<?>[]{ Int32Type.instance }, 
schemaTypes);
+    }
+
+    @Test
+    public void testComponentEqualityIsContentBased() throws Exception
+    {
+        Function<String, TableMetadata> schema = name -> 
TableMetadata.builder(KEYSPACE, name)
+                                                                      
.addPartitionKeyColumn("k", Int32Type.instance)
+                                                                      
.addClusteringColumn("c1", Int32Type.instance)
+                                                                      
.addClusteringColumn("c2", LongType.instance)
+                                                                      
.addRegularColumn("v", Int32Type.instance)
+                                                                      .build();
+
+        // distinct TableMetadata instances, hence distinct clusteringTypes 
arrays with equal content
+        SerializationHeader.Component component = 
SerializationHeader.makeWithoutStats(schema.apply("t")).toComponent();
+        SerializationHeader.Component same = 
SerializationHeader.makeWithoutStats(schema.apply("t")).toComponent();
+        Assert.assertEquals(component, same);
+        Assert.assertEquals(component.hashCode(), same.hashCode());
+
+        TableMetadata otherClustering = TableMetadata.builder(KEYSPACE, "t")
+                                                    
.addPartitionKeyColumn("k", Int32Type.instance)
+                                                    .addClusteringColumn("c1", 
Int32Type.instance)
+                                                    .addRegularColumn("v", 
Int32Type.instance)
+                                                    .build();
+        Assert.assertNotEquals(component, 
SerializationHeader.makeWithoutStats(otherClustering).toComponent());
+
+        SSTableFormat<?, ?> format = 
DatabaseDescriptor.getSelectedSSTableFormat();
+        try (DataOutputBuffer out = new DataOutputBuffer())
+        {
+            
SerializationHeader.serializer.serialize(format.getLatestVersion(), component, 
out);
+            SerializationHeader.Component deserialized =
+                
SerializationHeader.serializer.deserialize(format.getLatestVersion(), new 
DataInputBuffer(out.buffer(), true));
+            Assert.assertEquals(component, deserialized);
+            Assert.assertEquals(component.hashCode(), deserialized.hashCode());
+        }
+    }
+
+    @Test
+    public void testToStringRendersClusteringTypes() throws 
UnknownColumnException
+    {
+        TableMetadata schema = TableMetadata.builder(KEYSPACE, 
"testToStringRendersClusteringTypes")
+                                            .addPartitionKeyColumn("k", 
Int32Type.instance)
+                                            .addClusteringColumn("c1", 
Int32Type.instance)
+                                            .addClusteringColumn("c2", 
LongType.instance)
+                                            .build();
+
+        SerializationHeader.Component component = 
SerializationHeader.makeWithoutStats(schema).toComponent();
+        String expectedTypes = Int32Type.instance + ", " + LongType.instance;
+        Assert.assertTrue(component.toString(), 
component.toString().contains("cks=[" + expectedTypes + ']'));
+        Assert.assertTrue(component.toHeader(schema).toString(),
+                          
component.toHeader(schema).toString().contains("cks=[" + expectedTypes + ']'));
+    }
+
     /**
      * Common case: schema unchanged since the SSTable was written.
      */
@@ -324,4 +393,25 @@ public class SerializationHeaderTest
         }
     }
 
+    private static TableMetadata schemaWithClusteringColumns(int 
clusteringColumns)
+    {
+        TableMetadata.Builder builder = TableMetadata.builder(KEYSPACE, 
"testClusteringTypesContainer" + clusteringColumns)
+                                                     
.addPartitionKeyColumn("k", Int32Type.instance);
+        for (int i = 0; i < clusteringColumns; i++)
+            builder.addClusteringColumn("c" + i, Int32Type.instance);
+        return builder.build();
+    }
+
+    private static SerializationHeader deserializeHeader(SerializationHeader 
header, TableMetadata schema) throws Exception
+    {
+        SSTableFormat<?, ?> format = 
DatabaseDescriptor.getSelectedSSTableFormat();
+        try (DataOutputBuffer out = new DataOutputBuffer())
+        {
+            
SerializationHeader.serializer.serialize(format.getLatestVersion(), 
header.toComponent(), out);
+            return 
SerializationHeader.serializer.deserialize(format.getLatestVersion(),
+                                                              new 
DataInputBuffer(out.buffer(), true))
+                                                 .toHeader(schema);
+        }
+    }
+
 }
diff --git 
a/test/unit/org/apache/cassandra/db/compaction/StatefulCursorTest.java 
b/test/unit/org/apache/cassandra/db/compaction/StatefulCursorTest.java
index 33f5aedd5a..df12139885 100644
--- a/test/unit/org/apache/cassandra/db/compaction/StatefulCursorTest.java
+++ b/test/unit/org/apache/cassandra/db/compaction/StatefulCursorTest.java
@@ -346,7 +346,7 @@ public class StatefulCursorTest extends CQLTester
      */
     private static void driveReader(SSTableReader sstable, boolean 
overDeclareRowBody) throws Exception
     {
-        AbstractType<?>[] clusteringTypes = 
sstable.header.clusteringTypes().toArray(AbstractType[]::new);
+        AbstractType<?>[] clusteringTypes = sstable.header.clusteringTypes();
         UnfilteredDescriptor unfiltered = overDeclareRowBody
                                          ? new 
UnfilteredDescriptor(clusteringTypes)
                                            {
diff --git 
a/test/unit/org/apache/cassandra/db/compaction/differential/ComplexColumnCursorReadTest.java
 
b/test/unit/org/apache/cassandra/db/compaction/differential/ComplexColumnCursorReadTest.java
index 74fdafec33..ca0b2c71a1 100644
--- 
a/test/unit/org/apache/cassandra/db/compaction/differential/ComplexColumnCursorReadTest.java
+++ 
b/test/unit/org/apache/cassandra/db/compaction/differential/ComplexColumnCursorReadTest.java
@@ -26,7 +26,6 @@ import org.junit.Test;
 
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.db.ColumnFamilyStore;
-import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.rows.Cell;
 import org.apache.cassandra.db.rows.ColumnData;
 import org.apache.cassandra.db.rows.ComplexColumnData;
@@ -174,7 +173,7 @@ public class ComplexColumnCursorReadTest extends CQLTester
                 cursor.pauseAtEmptyComplexColumns(true);
 
             PartitionDescriptor pHeader = new 
PartitionDescriptor(sstable.getPartitioner().createReusableKey(0));
-            UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(sstable.header.clusteringTypes().toArray(AbstractType[]::new));
+            UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(sstable.header.clusteringTypes());
             byte[] transfer = new byte[4096];
 
             int state = cursor.readPartitionHeader(pHeader);
@@ -367,7 +366,7 @@ public class ComplexColumnCursorReadTest extends CQLTester
              DataOutputBuffer valueSink = new DataOutputBuffer())
         {
             PartitionDescriptor pHeader = new 
PartitionDescriptor(sstable.getPartitioner().createReusableKey(0));
-            UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(sstable.header.clusteringTypes().toArray(AbstractType[]::new));
+            UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(sstable.header.clusteringTypes());
             byte[] transfer = new byte[4096];
 
             int state = cursor.readPartitionHeader(pHeader);
diff --git a/test/unit/org/apache/cassandra/db/filter/SliceTest.java 
b/test/unit/org/apache/cassandra/db/filter/SliceTest.java
index 18233f09f5..db9754f00e 100644
--- a/test/unit/org/apache/cassandra/db/filter/SliceTest.java
+++ b/test/unit/org/apache/cassandra/db/filter/SliceTest.java
@@ -19,8 +19,6 @@
 package org.apache.cassandra.db.filter;
 
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.List;
 
 import org.junit.Test;
 
@@ -48,10 +46,10 @@ public class SliceTest
     @Test
     public void testIntersectsSingleSlice()
     {
-        List<AbstractType<?>> types = new ArrayList<>();
-        types.add(Int32Type.instance);
-        types.add(Int32Type.instance);
-        types.add(Int32Type.instance);
+        AbstractType<?>[] types = new AbstractType<?>[3];
+        types[0] = Int32Type.instance;
+        types[1] = Int32Type.instance;
+        types[2] = Int32Type.instance;
         ClusteringComparator cc = new ClusteringComparator(types);
 
         ClusteringPrefix.Kind sk = INCL_START_BOUND;
@@ -279,10 +277,10 @@ public class SliceTest
     @Test
     public void testDifferentMinMaxLengths()
     {
-        List<AbstractType<?>> types = new ArrayList<>();
-        types.add(Int32Type.instance);
-        types.add(Int32Type.instance);
-        types.add(Int32Type.instance);
+        AbstractType<?>[] types = new AbstractType<?>[3];
+        types[0] = Int32Type.instance;
+        types[1] = Int32Type.instance;
+        types[2] = Int32Type.instance;
         ClusteringComparator cc = new ClusteringComparator(types);
 
         ClusteringPrefix.Kind sk = INCL_START_BOUND;
@@ -321,10 +319,10 @@ public class SliceTest
     @Test
     public void testSliceNormalization()
     {
-        List<AbstractType<?>> types = new ArrayList<>();
-        types.add(Int32Type.instance);
-        types.add(Int32Type.instance);
-        types.add(Int32Type.instance);
+        AbstractType<?>[] types = new AbstractType<?>[3];
+        types[0] = Int32Type.instance;
+        types[1] = Int32Type.instance;
+        types[2] = Int32Type.instance;
         ClusteringComparator cc = new ClusteringComparator(types);
 
         assertSlicesNormalization(cc, slices(s(0, 2), s(2, 4)), slices(s(0, 
4)));
@@ -337,9 +335,9 @@ public class SliceTest
     @Test
     public void testIsEmpty()
     {
-        List<AbstractType<?>> types = new ArrayList<>();
-        types.add(Int32Type.instance);
-        types.add(Int32Type.instance);
+        AbstractType<?>[] types = new AbstractType<?>[2];
+        types[0] = Int32Type.instance;
+        types[1] = Int32Type.instance;
         ClusteringComparator cc = new ClusteringComparator(types);
 
         assertFalse(Slice.isEmpty(cc, makeBound(INCL_START_BOUND, 5, 0), 
makeBound(INCL_END_BOUND, 5, 0)));
@@ -406,4 +404,4 @@ public class SliceTest
         assertFalse(String.format("Slice %s should not intersect with slice 
%s", s1.toString(cc), s2.toString(cc)), s1.intersects(cc, s2));
         assertFalse(String.format("Slice %s should not intersect with slice 
%s", s2.toString(cc), s1.toString(cc)), s2.intersects(cc, s1));
     }
-}
\ No newline at end of file
+}
diff --git 
a/test/unit/org/apache/cassandra/io/compress/CompressedSequentialWriterTest.java
 
b/test/unit/org/apache/cassandra/io/compress/CompressedSequentialWriterTest.java
index bf5e9b83a2..e25c3e7c34 100644
--- 
a/test/unit/org/apache/cassandra/io/compress/CompressedSequentialWriterTest.java
+++ 
b/test/unit/org/apache/cassandra/io/compress/CompressedSequentialWriterTest.java
@@ -248,7 +248,7 @@ public class CompressedSequentialWriterTest extends 
SequentialWriterTest
     private void testWrite(File f, int bytesToTest, boolean useMemmap) throws 
IOException
     {
         final String filename = f.absolutePath();
-        MetadataCollector sstableMetadataCollector = new MetadataCollector(new 
ClusteringComparator(Collections.singletonList(BytesType.instance)));
+        MetadataCollector sstableMetadataCollector = new MetadataCollector(new 
ClusteringComparator(BytesType.instance));
 
         byte[] dataPre = new byte[bytesToTest];
         byte[] rawPost = new byte[bytesToTest];
@@ -348,7 +348,7 @@ public class CompressedSequentialWriterTest extends 
SequentialWriterTest
 
         File f = FileUtils.createTempFile("testUncompressedChunks", "1");
         String filename = f.path();
-        MetadataCollector sstableMetadataCollector = new MetadataCollector(new 
ClusteringComparator(Collections.singletonList(BytesType.instance)));
+        MetadataCollector sstableMetadataCollector = new MetadataCollector(new 
ClusteringComparator(BytesType.instance));
         compressionParameters = new 
CompressionParams(MockCompressor.class.getTypeName(),
                                                       
MockCompressor.paramsFor(ratio, extra),
                                                       DEFAULT_CHUNK_LENGTH, 
ratio);
diff --git 
a/test/unit/org/apache/cassandra/io/compress/DirectCompressedSequentialWriterTest.java
 
b/test/unit/org/apache/cassandra/io/compress/DirectCompressedSequentialWriterTest.java
index 68833f1530..d801d2487f 100644
--- 
a/test/unit/org/apache/cassandra/io/compress/DirectCompressedSequentialWriterTest.java
+++ 
b/test/unit/org/apache/cassandra/io/compress/DirectCompressedSequentialWriterTest.java
@@ -31,7 +31,6 @@ import java.nio.file.Files;
 import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -102,7 +101,7 @@ public class DirectCompressedSequentialWriterTest
 
     private static MetadataCollector newCollector()
     {
-        return new MetadataCollector(new 
ClusteringComparator(Collections.singletonList(BytesType.instance)));
+        return new MetadataCollector(new 
ClusteringComparator(BytesType.instance));
     }
 
     @FunctionalInterface
diff --git 
a/test/unit/org/apache/cassandra/io/sstable/SSTableCursorReaderCopyCellValueTest.java
 
b/test/unit/org/apache/cassandra/io/sstable/SSTableCursorReaderCopyCellValueTest.java
index be91d7604a..b4c14f456e 100644
--- 
a/test/unit/org/apache/cassandra/io/sstable/SSTableCursorReaderCopyCellValueTest.java
+++ 
b/test/unit/org/apache/cassandra/io/sstable/SSTableCursorReaderCopyCellValueTest.java
@@ -26,7 +26,6 @@ import org.junit.Test;
 import org.apache.cassandra.cache.ChunkCache;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.db.ColumnFamilyStore;
-import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.ByteBufferAccessor;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
@@ -224,7 +223,7 @@ public class SSTableCursorReaderCopyCellValueTest extends 
CQLTester
         {
             byte[] transfer = new byte[4096];
             PartitionDescriptor pHeader = new 
PartitionDescriptor(sstable.getPartitioner().createReusableKey(0));
-            UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(sstable.header.clusteringTypes().toArray(AbstractType[]::new));
+            UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(sstable.header.clusteringTypes());
 
             int state = cursor.readPartitionHeader(pHeader);
             byte[] copied = null;
diff --git 
a/test/unit/org/apache/cassandra/io/sstable/SSTableCursorReaderCorruptCellCountTest.java
 
b/test/unit/org/apache/cassandra/io/sstable/SSTableCursorReaderCorruptCellCountTest.java
index 057a7e73ed..806afb15e6 100644
--- 
a/test/unit/org/apache/cassandra/io/sstable/SSTableCursorReaderCorruptCellCountTest.java
+++ 
b/test/unit/org/apache/cassandra/io/sstable/SSTableCursorReaderCorruptCellCountTest.java
@@ -25,7 +25,6 @@ import org.junit.Test;
 import org.apache.cassandra.cache.ChunkCache;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.db.ColumnFamilyStore;
-import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.io.util.RandomAccessReader;
@@ -112,7 +111,7 @@ public class SSTableCursorReaderCorruptCellCountTest 
extends CQLTester
         try (SSTableCursorReader cursor = new SSTableCursorReader(sstable))
         {
             PartitionDescriptor pHeader = new 
PartitionDescriptor(sstable.getPartitioner().createReusableKey(0));
-            UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(sstable.header.clusteringTypes().toArray(AbstractType[]::new));
+            UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(sstable.header.clusteringTypes());
 
             int state = cursor.readPartitionHeader(pHeader);
             assertEquals("expected the single row to open directly", 
ROW_START, state);
@@ -152,7 +151,7 @@ public class SSTableCursorReaderCorruptCellCountTest 
extends CQLTester
         try (SSTableCursorReader cursor = new SSTableCursorReader(sstable))
         {
             PartitionDescriptor pHeader = new 
PartitionDescriptor(sstable.getPartitioner().createReusableKey(0));
-            UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(sstable.header.clusteringTypes().toArray(AbstractType[]::new));
+            UnfilteredDescriptor rHeader = new 
UnfilteredDescriptor(sstable.header.clusteringTypes());
             byte[] transfer = new byte[4096];
 
             int state = cursor.readPartitionHeader(pHeader);
diff --git 
a/test/unit/org/apache/cassandra/io/sstable/format/DataComponentDirectWriteSelectionTest.java
 
b/test/unit/org/apache/cassandra/io/sstable/format/DataComponentDirectWriteSelectionTest.java
index e55f364373..0bbf149904 100644
--- 
a/test/unit/org/apache/cassandra/io/sstable/format/DataComponentDirectWriteSelectionTest.java
+++ 
b/test/unit/org/apache/cassandra/io/sstable/format/DataComponentDirectWriteSelectionTest.java
@@ -19,7 +19,6 @@
 package org.apache.cassandra.io.sstable.format;
 
 import java.nio.file.Files;
-import java.util.Collections;
 
 import org.junit.After;
 import org.junit.Before;
@@ -158,7 +157,7 @@ public class DataComponentDirectWriteSelectionTest
     {
         Descriptor descriptor = new Descriptor(tmpDir, KS, CF, new 
SequenceBasedSSTableId(nextId++),
                                                
DatabaseDescriptor.getSelectedSSTableFormat());
-        MetadataCollector collector = new MetadataCollector(new 
ClusteringComparator(Collections.singletonList(BytesType.instance)));
+        MetadataCollector collector = new MetadataCollector(new 
ClusteringComparator(BytesType.instance));
         // finishOnClose(false) routes try-with-resources close through abort, 
avoiding finish() side effects on an empty writer.
         SequentialWriterOption options = 
SequentialWriterOption.newBuilder().finishOnClose(false).build();
         return DataComponent.buildWriter(descriptor, metadata, options, 
collector, op, FlushCompression.fast, null);
diff --git 
a/test/unit/org/apache/cassandra/io/sstable/format/big/RowIndexEntryTest.java 
b/test/unit/org/apache/cassandra/io/sstable/format/big/RowIndexEntryTest.java
index acd41cd111..ac853663e6 100644
--- 
a/test/unit/org/apache/cassandra/io/sstable/format/big/RowIndexEntryTest.java
+++ 
b/test/unit/org/apache/cassandra/io/sstable/format/big/RowIndexEntryTest.java
@@ -49,7 +49,6 @@ import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.LivenessInfo;
 import org.apache.cassandra.db.SerializationHeader;
 import org.apache.cassandra.db.TypeSizes;
-import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.LongType;
 import org.apache.cassandra.db.partitions.ImmutableBTreePartition;
 import org.apache.cassandra.db.rows.AbstractUnfilteredRowIterator;
@@ -89,8 +88,7 @@ import static org.junit.Assert.assertTrue;
 
 public class RowIndexEntryTest extends CQLTester
 {
-    private static final List<AbstractType<?>> clusterTypes = 
Collections.singletonList(LongType.instance);
-    private static final ClusteringComparator comp = new 
ClusteringComparator(clusterTypes);
+    private static final ClusteringComparator comp = new 
ClusteringComparator(LongType.instance);
 
     private static final byte[] dummy_100k = new byte[100000];
 
diff --git 
a/test/unit/org/apache/cassandra/serializers/AbstractTypeSerializerTest.java 
b/test/unit/org/apache/cassandra/serializers/AbstractTypeSerializerTest.java
new file mode 100644
index 0000000000..0495e7ccaa
--- /dev/null
+++ b/test/unit/org/apache/cassandra/serializers/AbstractTypeSerializerTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.cassandra.serializers;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+public class AbstractTypeSerializerTest
+{
+    private final AbstractTypeSerializer serializer = new 
AbstractTypeSerializer();
+
+    @Test
+    public void testSerializedArraySizeMatchesSerializeArray() throws 
IOException
+    {
+        assertRoundTrip();
+        assertRoundTrip(Int32Type.instance);
+        assertRoundTrip(Int32Type.instance, UTF8Type.instance);
+        assertRoundTrip(Int32Type.instance, UTF8Type.instance, 
Int32Type.instance, UTF8Type.instance);
+    }
+
+    private void assertRoundTrip(AbstractType<?>... types) throws IOException
+    {
+        try (DataOutputBuffer out = new DataOutputBuffer())
+        {
+            serializer.serializeArray(types, out);
+
+            assertEquals("serializedArraySize must match the number of bytes 
written by serializeArray",
+                         out.getLength(), 
serializer.serializedArraySize(types));
+
+            try (DataInputBuffer in = new DataInputBuffer(out.buffer(), true))
+            {
+                AbstractType<?>[] deserialized = 
serializer.deserializeArray(in);
+                assertArrayEquals(types, deserialized);
+            }
+        }
+    }
+}
diff --git 
a/test/unit/org/apache/cassandra/utils/bytecomparable/ByteSourceSequenceTest.java
 
b/test/unit/org/apache/cassandra/utils/bytecomparable/ByteSourceSequenceTest.java
index 3344424d96..b3a009be00 100644
--- 
a/test/unit/org/apache/cassandra/utils/bytecomparable/ByteSourceSequenceTest.java
+++ 
b/test/unit/org/apache/cassandra/utils/bytecomparable/ByteSourceSequenceTest.java
@@ -20,7 +20,6 @@ package org.apache.cassandra.utils.bytecomparable;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.nio.ByteBuffer;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Random;
@@ -298,35 +297,35 @@ public class ByteSourceSequenceTest
     private static final IntegerType VARINT = IntegerType.instance;
 
     // A regular comparator using the natural ordering for all types.
-    private static final ClusteringComparator COMP = new 
ClusteringComparator(Arrays.asList(
+    private static final ClusteringComparator COMP = new ClusteringComparator(
             UTF8,
             DECIMAL,
             VARINT
-    ));
+    );
     // A comparator that reverses the ordering for the first unknown length 
type
-    private static final ClusteringComparator COMP_REVERSED_UNKNOWN_LENGTH = 
new ClusteringComparator(Arrays.asList(
+    private static final ClusteringComparator COMP_REVERSED_UNKNOWN_LENGTH = 
new ClusteringComparator(
             ReversedType.getInstance(UTF8),
             DECIMAL,
             VARINT
-    ));
+    );
     // A comparator that reverses the ordering for the second unknown length 
type
-    private static final ClusteringComparator COMP_REVERSED_UNKNOWN_LENGTH_2 = 
new ClusteringComparator(Arrays.asList(
+    private static final ClusteringComparator COMP_REVERSED_UNKNOWN_LENGTH_2 = 
new ClusteringComparator(
             UTF8,
             ReversedType.getInstance(DECIMAL),
             VARINT
-    ));
+    );
     // A comparator that reverses the ordering for the sole known/computable 
length type
-    private static final ClusteringComparator COMP_REVERSED_KNOWN_LENGTH = new 
ClusteringComparator(Arrays.asList(
+    private static final ClusteringComparator COMP_REVERSED_KNOWN_LENGTH = new 
ClusteringComparator(
             UTF8,
             DECIMAL,
             ReversedType.getInstance(VARINT)
-    ));
+    );
     // A comparator that reverses the ordering for all types
-    private static final ClusteringComparator COMP_ALL_REVERSED = new 
ClusteringComparator(Arrays.asList(
+    private static final ClusteringComparator COMP_ALL_REVERSED = new 
ClusteringComparator(
             ReversedType.getInstance(UTF8),
             ReversedType.getInstance(DECIMAL),
             ReversedType.getInstance(VARINT)
-    ));
+    );
 
     @Test
     public void testClusteringPrefixBoundNormalAndReversed()
@@ -536,7 +535,7 @@ public class ByteSourceSequenceTest
         AbstractType<String> reversedStringType = 
ReversedType.getInstance(UTF8);
         AbstractType<BigDecimal> reversedDecimalType = 
ReversedType.getInstance(DECIMAL);
 
-        final ClusteringComparator comparator = new 
ClusteringComparator(Arrays.asList(
+        final ClusteringComparator comparator = new ClusteringComparator(
                 // unknown length type
                 UTF8,
                 // known length type
@@ -545,7 +544,7 @@ public class ByteSourceSequenceTest
                 reversedStringType,
                 // reversed known length type
                 reversedDecimalType
-        ));
+        );
         ByteBuffer[] clusteringKeyValues = new ByteBuffer[] {
                 UTF8.decompose(stringValue),
                 DECIMAL.decompose(decimalValue),
@@ -553,7 +552,7 @@ public class ByteSourceSequenceTest
                 DECIMAL.decompose(decimalValue)
         };
 
-        final ClusteringComparator comparator2 = new 
ClusteringComparator(Arrays.asList(
+        final ClusteringComparator comparator2 = new ClusteringComparator(
                 // known length type
                 DECIMAL,
                 // unknown length type
@@ -562,7 +561,7 @@ public class ByteSourceSequenceTest
                 reversedDecimalType,
                 // reversed unknown length type
                 reversedStringType
-        ));
+        );
         ByteBuffer[] clusteringKeyValues2 = new ByteBuffer[] {
                 DECIMAL.decompose(decimalValue),
                 UTF8.decompose(stringValue),


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to