You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pt...@apache.org on 2023/08/22 19:20:52 UTC

[ignite-3] branch main updated: IGNITE-19542 .NET: Add BinaryTupleIgniteTupleAdapter (#2479)

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

ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new d15b975b97 IGNITE-19542 .NET: Add BinaryTupleIgniteTupleAdapter (#2479)
d15b975b97 is described below

commit d15b975b97ed41465b9e8b7e855ba20bff2d7f81
Author: Pavel Tupitsyn <pt...@apache.org>
AuthorDate: Tue Aug 22 22:20:46 2023 +0300

    IGNITE-19542 .NET: Add BinaryTupleIgniteTupleAdapter (#2479)
    
    When reading data from server, currently we unpack all elements of the incoming `BinaryTuple` into `IgniteTuple`. This is extra work and extra allocations. Instead, create an adapter from `BinaryTuple` to `IIgniteTuple` like `MutableRowTupleAdapter` does in Java.
    
    **Benchmarks**
    
    ```
    BEFORE
    
    |             Method |      Mean |    Error |   StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
    |------------------- |----------:|---------:|---------:|------:|--------:|-------:|----------:|
    |   ReadObjectManual |  52.66 ns | 0.326 ns | 0.305 ns |  1.00 |    0.00 | 0.0003 |      80 B |
    |         ReadObject |  88.41 ns | 0.425 ns | 0.398 ns |  1.68 |    0.01 | 0.0002 |      80 B |
    |          ReadTuple | 263.26 ns | 2.822 ns | 2.502 ns |  5.00 |    0.06 | 0.0019 |     544 B |
    | ReadTupleAndFields | 268.03 ns | 3.866 ns | 3.616 ns |  5.09 |    0.09 | 0.0019 |     544 B |
    
    
    AFTER
    
    |             Method |      Mean |    Error |   StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
    |------------------- |----------:|---------:|---------:|------:|--------:|-------:|----------:|
    |   ReadObjectManual |  53.23 ns | 0.320 ns | 0.268 ns |  1.00 |    0.00 | 0.0003 |      80 B |
    |         ReadObject |  88.01 ns | 0.484 ns | 0.453 ns |  1.65 |    0.01 | 0.0002 |      80 B |
    |          ReadTuple |  23.66 ns | 0.274 ns | 0.257 ns |  0.44 |    0.00 | 0.0004 |     120 B |
    | ReadTupleAndFields | 136.34 ns | 2.014 ns | 1.884 ns |  2.56 |    0.04 | 0.0007 |     208 B |
    ```
    
    After this change, `ReadTuple` simply performs a buffer copy, but does not deserialize individual fields. `ReadTupleAndFields` unpacks all fields, and it is still faster and allocates less than before.
---
 .../dotnet/Apache.Ignite.Benchmarks/Program.cs     |   4 +-
 .../SerializerHandlerReadBenchmarks.cs             |  55 +++-----
 .../Table/BinaryTupleIgniteTupleAdapterTests.cs    | 111 ++++++++++++++++
 .../Apache.Ignite.Tests/Table/IgniteTupleTests.cs  |  36 +++---
 .../dotnet/Apache.Ignite.Tests/ToStringTests.cs    |   3 +-
 .../BinaryTuple/BinaryTupleIgniteTupleAdapter.cs   | 143 +++++++++++++++++++++
 .../Internal/Table/IgniteTupleCommon.cs}           |  32 ++++-
 .../Table/Serialization/TupleSerializerHandler.cs  |  41 +++++-
 .../dotnet/Apache.Ignite/Table/IIgniteTuple.cs     |  18 +++
 .../dotnet/Apache.Ignite/Table/IgniteTuple.cs      |  53 ++------
 10 files changed, 386 insertions(+), 110 deletions(-)

diff --git a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs
index 0c256c1385..366dde3ad2 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs
@@ -18,9 +18,9 @@
 namespace Apache.Ignite.Benchmarks;
 
 using BenchmarkDotNet.Running;
-using Table;
+using Table.Serialization;
 
 internal static class Program
 {
-    private static void Main() => BenchmarkRunner.Run<DataStreamerBenchmark>();
+    private static void Main() => BenchmarkRunner.Run<SerializerHandlerReadBenchmarks>();
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
index 5517e741c3..4ad9571b2a 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Table/Serialization/SerializerHandlerReadBenchmarks.cs
@@ -26,44 +26,14 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
     /// <summary>
     /// Benchmarks for <see cref="IRecordSerializerHandler{T}.Read"/> implementations.
     ///
-    /// Results on Intel Core i7-9700K, .NET SDK 3.1.416, Ubuntu 20.04:
-    /// |           Method |       Mean |   Error |  StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
-    /// |----------------- |-----------:|--------:|--------:|------:|--------:|-------:|----------:|
-    /// | ReadObjectManual |   210.9 ns | 0.73 ns | 0.65 ns |  1.00 |    0.00 | 0.0126 |      80 B |
-    /// |       ReadObject |   257.5 ns | 1.41 ns | 1.25 ns |  1.22 |    0.01 | 0.0124 |      80 B |
-    /// |        ReadTuple |   561.0 ns | 3.09 ns | 2.89 ns |  2.66 |    0.01 | 0.0849 |     536 B |
-    /// |    ReadObjectOld | 1,020.9 ns | 9.05 ns | 8.47 ns |  4.84 |    0.05 | 0.0744 |     472 B |.
+    /// Results on i9-12900H, .NET SDK 6.0.405, Ubuntu 22.04:
     ///
-    /// Results on i7-7700HQ, .NET SDK 6.0.400, Ubuntu 20.04:
-    /// MsgPack (old)
-    /// |           Method |     Mean |   Error |  StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
-    /// |----------------- |---------:|--------:|--------:|------:|--------:|-------:|----------:|
-    /// | ReadObjectManual | 289.4 ns | 2.92 ns | 2.59 ns |  1.00 |    0.00 | 0.0024 |      80 B |
-    /// |       ReadObject | 364.3 ns | 3.28 ns | 3.07 ns |  1.26 |    0.02 | 0.0024 |      80 B |
-    /// |        ReadTuple | 755.4 ns | 2.82 ns | 2.35 ns |  2.61 |    0.03 | 0.0181 |     536 B |
-    ///
-    /// BinaryTuple (new)
-    /// |           Method |     Mean |   Error |  StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
-    /// |----------------- |---------:|--------:|--------:|------:|--------:|-------:|----------:|
-    /// | ReadObjectManual | 299.3 ns | 3.42 ns | 3.20 ns |  1.00 |    0.00 | 0.0024 |      80 B |
-    /// |       ReadObject | 382.9 ns | 2.49 ns | 2.21 ns |  1.28 |    0.02 | 0.0024 |      80 B |
-    /// |        ReadTuple | 769.0 ns | 6.06 ns | 5.37 ns |  2.57 |    0.04 | 0.0181 |     536 B |.
-    ///
-    /// Comparison of MessagePack library and our own implementation, i9-12900H, .NET SDK 6.0.405, Ubuntu 22.04:
-    ///
-    /// MessagePack 2.1.90 (old)
-    /// |           Method |     Mean |   Error |  StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
-    /// |----------------- |---------:|--------:|--------:|------:|--------:|-------:|----------:|
-    /// | ReadObjectManual | 100.3 ns | 0.46 ns | 0.41 ns |  1.00 |    0.00 | 0.0002 |      80 B |
-    /// |       ReadObject | 142.3 ns | 0.35 ns | 0.31 ns |  1.42 |    0.01 | 0.0002 |      80 B |
-    /// |        ReadTuple | 266.8 ns | 2.52 ns | 2.35 ns |  2.66 |    0.03 | 0.0019 |     544 B |.
-    ///
-    /// Custom MsgPackReader (new)
-    /// |           Method |      Mean |    Error |   StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
-    /// |----------------- |----------:|---------:|---------:|------:|--------:|-------:|----------:|
-    /// | ReadObjectManual |  38.30 ns | 0.265 ns | 0.247 ns |  1.00 |    0.00 | 0.0003 |      80 B |
-    /// |       ReadObject |  80.51 ns | 0.158 ns | 0.124 ns |  2.10 |    0.01 | 0.0002 |      80 B |
-    /// |        ReadTuple | 208.63 ns | 0.654 ns | 0.611 ns |  5.45 |    0.04 | 0.0019 |     544 B |.
+    /// |             Method |      Mean |    Error |   StdDev | Ratio | RatioSD |  Gen 0 | Allocated |
+    /// |------------------- |----------:|---------:|---------:|------:|--------:|-------:|----------:|
+    /// |   ReadObjectManual |  53.23 ns | 0.320 ns | 0.268 ns |  1.00 |    0.00 | 0.0003 |      80 B |
+    /// |         ReadObject |  88.01 ns | 0.484 ns | 0.453 ns |  1.65 |    0.01 | 0.0002 |      80 B |
+    /// |          ReadTuple |  23.66 ns | 0.274 ns | 0.257 ns |  0.44 |    0.00 | 0.0004 |     120 B |
+    /// | ReadTupleAndFields | 136.34 ns | 2.014 ns | 1.884 ns |  2.56 |    0.04 | 0.0007 |     208 B |.
     /// </summary>
     [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Benchmarks.")]
     [MemoryDiagnoser]
@@ -102,5 +72,16 @@ namespace Apache.Ignite.Benchmarks.Table.Serialization
 
             Consumer.Consume(res);
         }
+
+        [Benchmark]
+        public void ReadTupleAndFields()
+        {
+            var reader = new MsgPackReader(SerializedData);
+            var res = TupleSerializerHandler.Instance.Read(ref reader, Schema);
+
+            Consumer.Consume(res[0]);
+            Consumer.Consume(res[1]);
+            Consumer.Consume(res[2]);
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/BinaryTupleIgniteTupleAdapterTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/BinaryTupleIgniteTupleAdapterTests.cs
new file mode 100644
index 0000000000..cd6bb62f3e
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/BinaryTupleIgniteTupleAdapterTests.cs
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Tests.Table;
+
+using System;
+using System.Collections.Generic;
+using Ignite.Sql;
+using Ignite.Table;
+using Internal.Proto.BinaryTuple;
+using Internal.Table;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests for <see cref="BinaryTupleIgniteTupleAdapter"/>. Ensures consistency with <see cref="IgniteTuple"/>.
+/// </summary>
+[TestFixture]
+public class BinaryTupleIgniteTupleAdapterTests : IgniteTupleTests
+{
+    [Test]
+    public void TestUpdate()
+    {
+        var tuple = CreateTuple(new IgniteTuple
+        {
+            ["A"] = 1,
+            ["B"] = "2",
+            ["C"] = 3L,
+            ["D"] = null
+        });
+
+        Assert.AreEqual(4, tuple.FieldCount);
+
+        Assert.AreEqual(1, tuple["A"]);
+        Assert.AreEqual("2", tuple["B"]);
+        Assert.AreEqual(3L, tuple["C"]);
+        Assert.IsNull(tuple["D"]);
+
+        Assert.IsNull(tuple.GetFieldValue<object>("_tuple"));
+        Assert.IsNotNull(tuple.GetFieldValue<object>("_schema"));
+
+        tuple["A"] = 11;
+
+        Assert.AreEqual(4, tuple.FieldCount);
+
+        Assert.AreEqual(11, tuple["A"]);
+        Assert.AreEqual("2", tuple["B"]);
+        Assert.AreEqual(3L, tuple["C"]);
+        Assert.IsNull(tuple["D"]);
+
+        Assert.IsNotNull(tuple.GetFieldValue<object>("_tuple"));
+        Assert.IsNull(tuple.GetFieldValue<object>("_schema"));
+    }
+
+    protected override string GetShortClassName() => nameof(BinaryTupleIgniteTupleAdapter);
+
+    protected override IIgniteTuple CreateTuple(IIgniteTuple source)
+    {
+        var cols = new List<Column>();
+        var builder = new BinaryTupleBuilder(source.FieldCount);
+
+        for (var i = 0; i < source.FieldCount; i++)
+        {
+            var name = source.GetName(i);
+            var val = source[i]!;
+            var type = GetColumnType(val);
+            var col = new Column(name, type, true, false, 0, i, 0, 0);
+
+            cols.Add(col);
+            builder.AppendObject(val, type);
+        }
+
+        var buf = builder.Build();
+        var schema = new Schema(0, 0, 0, 0, cols);
+
+        return new BinaryTupleIgniteTupleAdapter(buf, schema, cols.Count);
+
+        static ColumnType GetColumnType(object? obj)
+        {
+            if (obj == null || obj is string)
+            {
+                return ColumnType.String;
+            }
+
+            if (obj is int)
+            {
+                return ColumnType.Int32;
+            }
+
+            if (obj is long)
+            {
+                return ColumnType.Int64;
+            }
+
+            throw new NotSupportedException("Unsupported type: " + obj.GetType());
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs
index a2eb5f6157..40ed2d7149 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/IgniteTupleTests.cs
@@ -30,7 +30,7 @@ namespace Apache.Ignite.Tests.Table
         [Test]
         public void TestCreateUpdateRead()
         {
-            IIgniteTuple tuple = new IgniteTuple();
+            IIgniteTuple tuple = CreateTuple(new IgniteTuple());
             Assert.AreEqual(0, tuple.FieldCount);
 
             tuple["foo"] = 1;
@@ -68,7 +68,7 @@ namespace Apache.Ignite.Tests.Table
         [Test]
         public void TestGetNullOrEmptyNameThrowsException()
         {
-            var tuple = new IgniteTuple { ["Foo"] = 1 };
+            var tuple = CreateTuple(new IgniteTuple { ["Foo"] = 1 });
 
             var ex = Assert.Throws<ArgumentException>(() => tuple.GetOrdinal(string.Empty));
             Assert.AreEqual("Column name can not be null or empty.", ex!.Message);
@@ -92,46 +92,44 @@ namespace Apache.Ignite.Tests.Table
         [Test]
         public void TestGetNonExistingNameThrowsException()
         {
-            var tuple = new IgniteTuple { ["Foo"] = 1 };
+            var tuple = CreateTuple(new IgniteTuple { ["Foo"] = 1 });
 
-            var ex = Assert.Throws<KeyNotFoundException>(() =>
-            {
-                var unused = tuple["bar"];
-            });
+            var ex = Assert.Throws<KeyNotFoundException>(() => { _ = tuple["bar"]; });
             Assert.AreEqual("The given key 'BAR' was not present in the dictionary.", ex!.Message);
         }
 
         [Test]
         public void TestToStringEmpty()
         {
-            Assert.AreEqual("IgniteTuple { }", new IgniteTuple().ToString());
+            var tuple = CreateTuple(new IgniteTuple());
+            Assert.AreEqual(GetShortClassName() + " { }", tuple.ToString());
         }
 
         [Test]
         public void TestToStringOneField()
         {
-            var tuple = new IgniteTuple { ["foo"] = 1 };
-            Assert.AreEqual("IgniteTuple { FOO = 1 }", tuple.ToString());
+            var tuple = CreateTuple(new IgniteTuple { ["foo"] = 1 });
+            Assert.AreEqual(GetShortClassName() + " { FOO = 1 }", tuple.ToString());
         }
 
         [Test]
         public void TestToStringTwoFields()
         {
-            var tuple = new IgniteTuple
+            var tuple = CreateTuple(new IgniteTuple
             {
                 ["foo"] = 1,
                 ["b"] = "abcd"
-            };
+            });
 
-            Assert.AreEqual("IgniteTuple { FOO = 1, B = abcd }", tuple.ToString());
+            Assert.AreEqual(GetShortClassName() + " { FOO = 1, B = abcd }", tuple.ToString());
         }
 
         [Test]
         public void TestEquality()
         {
-            var t1 = new IgniteTuple(2) { ["k"] = 1, ["v"] = "2" };
-            var t2 = new IgniteTuple(3) { ["k"] = 1, ["v"] = "2" };
-            var t3 = new IgniteTuple(4) { ["k"] = 1, ["v"] = null };
+            var t1 = CreateTuple(new IgniteTuple(2) { ["k"] = 1, ["v"] = "2" });
+            var t2 = CreateTuple(new IgniteTuple(3) { ["k"] = 1, ["v"] = "2" });
+            var t3 = CreateTuple(new IgniteTuple(4) { ["k"] = 1, ["v"] = null });
 
             Assert.AreEqual(t1, t2);
             Assert.AreEqual(t2, t1);
@@ -147,11 +145,15 @@ namespace Apache.Ignite.Tests.Table
         [Test]
         public void TestCustomTupleEquality()
         {
-            var tuple = new IgniteTuple { ["key"] = 42L, ["val"] = "Val1" };
+            var tuple = CreateTuple(new IgniteTuple { ["key"] = 42L, ["val"] = "Val1" });
             var customTuple = new CustomTestIgniteTuple();
 
             Assert.IsTrue(IIgniteTuple.Equals(tuple, customTuple));
             Assert.AreEqual(IIgniteTuple.GetHashCode(tuple), IIgniteTuple.GetHashCode(customTuple));
         }
+
+        protected virtual string GetShortClassName() => nameof(IgniteTuple);
+
+        protected virtual IIgniteTuple CreateTuple(IIgniteTuple source) => source;
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/ToStringTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/ToStringTests.cs
index 847b934132..8194e2bc80 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/ToStringTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/ToStringTests.cs
@@ -52,7 +52,8 @@ public class ToStringTests
                 var code = File.ReadAllText(path);
 
                 if (code.Contains("new IgniteToStringBuilder(", StringComparison.Ordinal) ||
-                    code.Contains("IgniteToStringBuilder.Build(", StringComparison.Ordinal))
+                    code.Contains("IgniteToStringBuilder.Build(", StringComparison.Ordinal) ||
+                    code.Contains("IIgniteTuple.ToString(this)", StringComparison.Ordinal))
                 {
                     continue;
                 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleIgniteTupleAdapter.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleIgniteTupleAdapter.cs
new file mode 100644
index 0000000000..2e4230be28
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Proto/BinaryTuple/BinaryTupleIgniteTupleAdapter.cs
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Internal.Proto.BinaryTuple;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Ignite.Table;
+using Table;
+using Table.Serialization;
+
+/// <summary>
+/// Adapts <see cref="BinaryTuple"/> to <see cref="IIgniteTuple"/>, so that we can avoid extra copying and allocations when
+/// reading data from the server.
+/// </summary>
+internal sealed class BinaryTupleIgniteTupleAdapter : IIgniteTuple, IEquatable<BinaryTupleIgniteTupleAdapter>, IEquatable<IIgniteTuple>
+{
+    private readonly int _schemaFieldCount; // Key-only tuples have less fields than schema.
+
+    private Memory<byte> _data;
+
+    private Schema? _schema;
+
+    private Dictionary<string, int>? _indexes;
+
+    private IgniteTuple? _tuple;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="BinaryTupleIgniteTupleAdapter"/> class.
+    /// </summary>
+    /// <param name="data">Binary tuple data.</param>
+    /// <param name="schema">Schema.</param>
+    /// <param name="fieldCount">Field count.</param>
+    public BinaryTupleIgniteTupleAdapter(Memory<byte> data, Schema schema, int fieldCount)
+    {
+        Debug.Assert(fieldCount <= schema.Columns.Count, "fieldCount <= schema.Columns.Count");
+
+        _data = data;
+        _schema = schema;
+        _schemaFieldCount = fieldCount;
+    }
+
+    /// <inheritdoc/>
+    public int FieldCount => _tuple?.FieldCount ?? _schemaFieldCount;
+
+    /// <inheritdoc/>
+    public object? this[int ordinal]
+    {
+        get => _tuple != null
+            ? _tuple[ordinal]
+            : TupleSerializerHandler.ReadObject(_data.Span, _schema!, _schemaFieldCount, ordinal);
+
+        set => InitTuple()[ordinal] = value;
+    }
+
+    /// <inheritdoc/>
+    public object? this[string name]
+    {
+        get => GetOrdinal(name) switch
+        {
+            var ordinal and >= 0 => this[ordinal],
+            _ => throw new KeyNotFoundException(
+                $"The given key '{IgniteTupleCommon.ParseColumnName(name)}' was not present in the dictionary.")
+        };
+        set => InitTuple()[name] = value;
+    }
+
+    /// <inheritdoc/>
+    public string GetName(int ordinal) => _schema != null
+        ? _schema.Columns[ordinal].Name
+        : _tuple!.GetName(ordinal);
+
+    /// <inheritdoc/>
+    public int GetOrdinal(string name)
+    {
+        if (_tuple != null)
+        {
+            return _tuple.GetOrdinal(name);
+        }
+
+        if (_indexes == null)
+        {
+            _indexes = new Dictionary<string, int>(_schema!.Columns.Count);
+
+            for (var i = 0; i < _schema.Columns.Count; i++)
+            {
+                _indexes[IgniteTupleCommon.ParseColumnName(_schema.Columns[i].Name)] = i;
+            }
+        }
+
+        return _indexes.TryGetValue(IgniteTupleCommon.ParseColumnName(name), out var index) ? index : -1;
+    }
+
+    /// <inheritdoc/>
+    public override string ToString() => IIgniteTuple.ToString(this);
+
+    /// <inheritdoc />
+    public bool Equals(BinaryTupleIgniteTupleAdapter? other) => IIgniteTuple.Equals(this, other);
+
+    /// <inheritdoc />
+    public bool Equals(IIgniteTuple? other) => IIgniteTuple.Equals(this, other);
+
+    /// <inheritdoc />
+    public override bool Equals(object? obj) => obj is IIgniteTuple other && Equals(other);
+
+    /// <inheritdoc />
+    public override int GetHashCode() => IIgniteTuple.GetHashCode(this);
+
+    private IIgniteTuple InitTuple()
+    {
+        if (_tuple != null)
+        {
+            return _tuple;
+        }
+
+        Debug.Assert(_schema != null, "_schema != null");
+
+        // Copy data to a mutable IgniteTuple.
+        _tuple = TupleSerializerHandler.ReadTuple(_data.Span, _schema, _schemaFieldCount);
+
+        // Release schema and data.
+        _schema = default;
+        _indexes = default;
+        _data = default;
+
+        return _tuple;
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/IgniteTupleCommon.cs
similarity index 52%
copy from modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs
copy to modules/platforms/dotnet/Apache.Ignite/Internal/Table/IgniteTupleCommon.cs
index 0c256c1385..5cdacd83ef 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Benchmarks/Program.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/IgniteTupleCommon.cs
@@ -1,4 +1,4 @@
-/*
+/*
  * 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.
@@ -15,12 +15,32 @@
  * limitations under the License.
  */
 
-namespace Apache.Ignite.Benchmarks;
+namespace Apache.Ignite.Internal.Table;
 
-using BenchmarkDotNet.Running;
-using Table;
+using System;
 
-internal static class Program
+/// <summary>
+/// Common tuple utilities.
+/// </summary>
+internal static class IgniteTupleCommon
 {
-    private static void Main() => BenchmarkRunner.Run<DataStreamerBenchmark>();
+    /// <summary>
+    /// Parses column name. Removes quotes and converts to upper case.
+    /// </summary>
+    /// <param name="name">Name.</param>
+    /// <returns>Parsed name.</returns>
+    public static string ParseColumnName(string name)
+    {
+        if (string.IsNullOrEmpty(name))
+        {
+            throw new ArgumentException("Column name can not be null or empty.");
+        }
+
+        if (name.Length > 2 && name.StartsWith('"') && name.EndsWith('"'))
+        {
+            return name.Substring(1, name.Length - 2);
+        }
+
+        return name.ToUpperInvariant();
+    }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
index 8158148867..b919f39a16 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/TupleSerializerHandler.cs
@@ -19,6 +19,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
 {
     using System;
     using System.Collections.Generic;
+    using System.Diagnostics;
     using System.Linq;
     using Common;
     using Ignite.Table;
@@ -43,13 +44,20 @@ namespace Apache.Ignite.Internal.Table.Serialization
             // No-op.
         }
 
-        /// <inheritdoc/>
-        public IIgniteTuple Read(ref MsgPackReader reader, Schema schema, bool keyOnly = false)
+        /// <summary>
+        /// Reads tuple from the buffer.
+        /// </summary>
+        /// <param name="buf">Buffer.</param>
+        /// <param name="schema">Schema.</param>
+        /// <param name="count">Column count to read.</param>
+        /// <returns>Tuple.</returns>
+        public static IgniteTuple ReadTuple(ReadOnlySpan<byte> buf, Schema schema, int count)
         {
-            var columns = schema.Columns;
-            var count = keyOnly ? schema.KeyColumnCount : columns.Count;
+            Debug.Assert(count <= schema.Columns.Count, "count <= schema.Columns.Count");
+
             var tuple = new IgniteTuple(count);
-            var tupleReader = new BinaryTupleReader(reader.ReadBinary(), count);
+            var tupleReader = new BinaryTupleReader(buf, count);
+            var columns = schema.Columns;
 
             for (var index = 0; index < count; index++)
             {
@@ -60,6 +68,29 @@ namespace Apache.Ignite.Internal.Table.Serialization
             return tuple;
         }
 
+        /// <summary>
+        /// Reads single column from the binary tuple.
+        /// </summary>
+        /// <param name="buf">Binary tuple buffer.</param>
+        /// <param name="schema">Schema.</param>
+        /// <param name="count">Column count.</param>
+        /// <param name="index">Column index.</param>
+        /// <returns>Column value.</returns>
+        public static object? ReadObject(ReadOnlySpan<byte> buf, Schema schema, int count, int index)
+        {
+            var tupleReader = new BinaryTupleReader(buf, count);
+            var column = schema.Columns[index];
+
+            return tupleReader.GetObject(index, column.Type, column.Scale);
+        }
+
+        /// <inheritdoc/>
+        public IIgniteTuple Read(ref MsgPackReader reader, Schema schema, bool keyOnly = false) =>
+            new BinaryTupleIgniteTupleAdapter(
+                data: reader.ReadBinary().ToArray(),
+                schema: schema,
+                fieldCount: keyOnly ? schema.KeyColumnCount : schema.Columns.Count);
+
         /// <inheritdoc/>
         public void Write(ref BinaryTupleBuilder tupleBuilder, IIgniteTuple record, Schema schema, int columnCount, Span<byte> noValueSet)
         {
diff --git a/modules/platforms/dotnet/Apache.Ignite/Table/IIgniteTuple.cs b/modules/platforms/dotnet/Apache.Ignite/Table/IIgniteTuple.cs
index fcb338101e..44fff1b2cd 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Table/IIgniteTuple.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Table/IIgniteTuple.cs
@@ -128,5 +128,23 @@ namespace Apache.Ignite.Table
 
             return true;
         }
+
+        /// <summary>
+        /// Converts tuple to string.
+        /// </summary>
+        /// <param name="tuple">Tuple.</param>
+        /// <returns>String representation.</returns>
+        public static string ToString(IIgniteTuple tuple)
+        {
+            IgniteArgumentCheck.NotNull(tuple, nameof(tuple));
+            var builder = new IgniteToStringBuilder(tuple.GetType());
+
+            for (var i = 0; i < tuple.FieldCount; i++)
+            {
+                builder.Append(tuple[i], tuple.GetName(i));
+            }
+
+            return builder.Build();
+        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Table/IgniteTuple.cs b/modules/platforms/dotnet/Apache.Ignite/Table/IgniteTuple.cs
index 1ba36e86c3..937f60a24f 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Table/IgniteTuple.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Table/IgniteTuple.cs
@@ -20,12 +20,12 @@ namespace Apache.Ignite.Table
     using System;
     using System.Collections.Generic;
     using System.Diagnostics.CodeAnalysis;
-    using Internal.Common;
+    using Internal.Table;
 
     /// <summary>
     /// Ignite tuple.
     /// </summary>
-    public sealed class IgniteTuple : IIgniteTuple, IEquatable<IgniteTuple>
+    public sealed class IgniteTuple : IIgniteTuple, IEquatable<IgniteTuple>, IEquatable<IIgniteTuple>
     {
         /** Key-value pairs. */
         [SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists", Justification = "Private.")]
@@ -57,10 +57,10 @@ namespace Apache.Ignite.Table
         /// <inheritdoc/>
         public object? this[string name]
         {
-            get => _pairs[_indexes[ParseName(name)]].Value;
+            get => _pairs[_indexes[IgniteTupleCommon.ParseColumnName(name)]].Value;
             set
             {
-                name = ParseName(name);
+                name = IgniteTupleCommon.ParseColumnName(name);
 
                 var pair = (name, value);
 
@@ -81,52 +81,21 @@ namespace Apache.Ignite.Table
         public string GetName(int ordinal) => _pairs[ordinal].Key;
 
         /// <inheritdoc/>
-        public int GetOrdinal(string name) => _indexes.TryGetValue(ParseName(name), out var index) ? index : -1;
+        public int GetOrdinal(string name) => _indexes.TryGetValue(IgniteTupleCommon.ParseColumnName(name), out var index) ? index : -1;
 
         /// <inheritdoc />
-        public override string ToString()
-        {
-            var builder = new IgniteToStringBuilder(GetType());
-
-            for (var i = 0; i < FieldCount; i++)
-            {
-                builder.Append(this[i], GetName(i));
-            }
-
-            return builder.Build();
-        }
+        public override string ToString() => IIgniteTuple.ToString(this);
 
         /// <inheritdoc />
-        public bool Equals(IgniteTuple? other)
-        {
-            return IIgniteTuple.Equals(this, other);
-        }
+        public bool Equals(IgniteTuple? other) => IIgniteTuple.Equals(this, other);
 
         /// <inheritdoc />
-        public override bool Equals(object? obj)
-        {
-            return obj is IgniteTuple other && Equals(other);
-        }
+        public bool Equals(IIgniteTuple? other) => IIgniteTuple.Equals(this, other);
 
         /// <inheritdoc />
-        public override int GetHashCode()
-        {
-            return IIgniteTuple.GetHashCode(this);
-        }
-
-        private static string ParseName(string name)
-        {
-            if (string.IsNullOrEmpty(name))
-            {
-                throw new ArgumentException("Column name can not be null or empty.");
-            }
+        public override bool Equals(object? obj) => obj is IIgniteTuple other && Equals(other);
 
-            if (name.Length > 2 && name.StartsWith('"') && name.EndsWith('"'))
-            {
-                return name.Substring(1, name.Length - 2);
-            }
-
-            return name.ToUpperInvariant();
-        }
+        /// <inheritdoc />
+        public override int GetHashCode() => IIgniteTuple.GetHashCode(this);
     }
 }