Skip to content

Commit a1b0088

Browse files
PascalSenncopybara-github
authored andcommitted
Adds UnsafeCollectionOperations for unsafe access to RepeatedField<T> (#16772)
This is a proposal to add `UnsafeCollectionOperations ` for fast access on `RepeatedField<T>` #16745 Closes #16772 COPYBARA_INTEGRATE_REVIEW=#16772 from PascalSenn:pse/readonlyspan-proposal-repeated-field dcb862a PiperOrigin-RevId: 702356972
1 parent b2acbd3 commit a1b0088

File tree

3 files changed

+350
-0
lines changed

3 files changed

+350
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#region Copyright notice and license
2+
3+
// Protocol Buffers - Google's data interchange format
4+
// Copyright 2015 Google Inc. All rights reserved.
5+
//
6+
// Use of this source code is governed by a BSD-style
7+
// license that can be found in the LICENSE file or at
8+
// https://developers.google.com/open-source/licenses/bsd
9+
10+
#endregion
11+
12+
using System;
13+
using System.Linq;
14+
using System.Runtime.CompilerServices;
15+
using System.Runtime.InteropServices;
16+
using NUnit.Framework;
17+
18+
namespace Google.Protobuf.Collections;
19+
20+
public class UnsafeCollectionOperationsTest
21+
{
22+
[Test]
23+
public void NullFieldAsSpanValueType()
24+
{
25+
RepeatedField<int> field = null;
26+
Assert.Throws<ArgumentNullException>(() => UnsafeCollectionOperations.AsSpan(field));
27+
}
28+
29+
[Test]
30+
public void NullFieldAsSpanClass()
31+
{
32+
RepeatedField<object> field = null;
33+
Assert.Throws<ArgumentNullException>(() => UnsafeCollectionOperations.AsSpan(field));
34+
}
35+
36+
[Test]
37+
public void FieldAsSpanValueType()
38+
{
39+
var field = new RepeatedField<int>();
40+
foreach (var length in Enumerable.Range(0, 36))
41+
{
42+
field.Clear();
43+
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
44+
45+
for (var i = 0; i < length; i++)
46+
{
47+
field.Add(i);
48+
}
49+
50+
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
51+
52+
field.Add(length + 1);
53+
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
54+
}
55+
56+
static void ValidateContentEquality(RepeatedField<int> field, Span<int> span)
57+
{
58+
Assert.AreEqual(field.Count, span.Length);
59+
60+
for (var i = 0; i < span.Length; i++)
61+
{
62+
Assert.AreEqual(field[i], span[i]);
63+
}
64+
}
65+
}
66+
67+
[Test]
68+
public void FieldAsSpanClass()
69+
{
70+
var field = new RepeatedField<IntAsObject>();
71+
foreach (var length in Enumerable.Range(0, 36))
72+
{
73+
field.Clear();
74+
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
75+
76+
for (var i = 0; i < length; i++)
77+
{
78+
field.Add(new IntAsObject { Value = i });
79+
}
80+
81+
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
82+
83+
field.Add(new IntAsObject { Value = length + 1 });
84+
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
85+
}
86+
87+
static void ValidateContentEquality(
88+
RepeatedField<IntAsObject> field,
89+
Span<IntAsObject> span)
90+
{
91+
Assert.AreEqual(field.Count, span.Length);
92+
93+
for (var i = 0; i < span.Length; i++)
94+
{
95+
Assert.AreEqual(field[i].Value, span[i].Value);
96+
}
97+
}
98+
}
99+
100+
[Test]
101+
public void FieldAsSpanLinkBreaksOnResize()
102+
{
103+
var field = new RepeatedField<int>();
104+
105+
for (var i = 0; i < 8; i++)
106+
{
107+
field.Add(i);
108+
}
109+
110+
var span = UnsafeCollectionOperations.AsSpan(field);
111+
112+
var startCapacity = field.Capacity;
113+
var startCount = field.Count;
114+
Assert.AreEqual(startCount, startCapacity);
115+
Assert.AreEqual(startCount, span.Length);
116+
117+
for (var i = 0; i < span.Length; i++)
118+
{
119+
span[i]++;
120+
Assert.AreEqual(field[i], span[i]);
121+
122+
field[i]++;
123+
Assert.AreEqual(field[i], span[i]);
124+
}
125+
126+
// Resize to break link between Span and RepeatedField
127+
field.Add(11);
128+
129+
Assert.AreNotEqual(startCapacity, field.Capacity);
130+
Assert.AreNotEqual(startCount, field.Count);
131+
Assert.AreEqual(startCount, span.Length);
132+
133+
for (var i = 0; i < span.Length; i++)
134+
{
135+
span[i] += 2;
136+
Assert.AreNotEqual(field[i], span[i]);
137+
138+
field[i] += 3;
139+
Assert.AreNotEqual(field[i], span[i]);
140+
}
141+
}
142+
143+
[Test]
144+
public void FieldSetCount()
145+
{
146+
RepeatedField<int> field = null;
147+
Assert.Throws<ArgumentNullException>(() => UnsafeCollectionOperations.SetCount(field, 3));
148+
149+
field = new RepeatedField<int>();
150+
Assert.Throws<ArgumentOutOfRangeException>(()
151+
=> UnsafeCollectionOperations.SetCount(field, -1));
152+
153+
UnsafeCollectionOperations.SetCount(field, 5);
154+
Assert.AreEqual(5, field.Count);
155+
156+
field = new RepeatedField<int> { 1, 2, 3, 4, 5 };
157+
ref var intRef = ref MemoryMarshal.GetReference(UnsafeCollectionOperations.AsSpan(field));
158+
159+
// make sure that size decrease preserves content
160+
UnsafeCollectionOperations.SetCount(field, 3);
161+
Assert.AreEqual(3, field.Count);
162+
Assert.Throws<ArgumentOutOfRangeException>(() => field[3] = 42);
163+
var span = UnsafeCollectionOperations.AsSpan(field);
164+
SequenceEqual(span, new[] { 1, 2, 3 });
165+
Assert.True(Unsafe.AreSame(ref intRef, ref MemoryMarshal.GetReference(span)));
166+
167+
// make sure that size increase preserves content and doesn't clear
168+
UnsafeCollectionOperations.SetCount(field, 5);
169+
span = UnsafeCollectionOperations.AsSpan(field);
170+
// .NET Framework always clears values. .NET 6+ only clears references.
171+
var expected =
172+
#if NET5_0_OR_GREATER
173+
new[] { 1, 2, 3, 4, 5 };
174+
#else
175+
new[] { 1, 2, 3, 0, 0 };
176+
#endif
177+
SequenceEqual(span, expected);
178+
Assert.True(Unsafe.AreSame(ref intRef, ref MemoryMarshal.GetReference(span)));
179+
180+
// make sure that reallocations preserve content
181+
var newCount = field.Capacity * 2;
182+
UnsafeCollectionOperations.SetCount(field, newCount);
183+
Assert.AreEqual(newCount, field.Count);
184+
span = UnsafeCollectionOperations.AsSpan(field);
185+
SequenceEqual(span.Slice(0, 3), new[] { 1, 2, 3 });
186+
Assert.True(!Unsafe.AreSame(ref intRef, ref MemoryMarshal.GetReference(span)));
187+
188+
RepeatedField<string> listReference = new() { "a", "b", "c", "d", "e" };
189+
var listSpan = UnsafeCollectionOperations.AsSpan(listReference);
190+
ref var stringRef = ref MemoryMarshal.GetReference(listSpan);
191+
UnsafeCollectionOperations.SetCount(listReference, 3);
192+
193+
// verify that reference types aren't cleared
194+
listSpan = UnsafeCollectionOperations.AsSpan(listReference);
195+
SequenceEqual(listSpan, new[] { "a", "b", "c" });
196+
Assert.True(Unsafe.AreSame(ref stringRef, ref MemoryMarshal.GetReference(listSpan)));
197+
UnsafeCollectionOperations.SetCount(listReference, 5);
198+
199+
// verify that removed reference types are cleared
200+
listSpan = UnsafeCollectionOperations.AsSpan(listReference);
201+
SequenceEqual(listSpan, new[] { "a", "b", "c", null, null });
202+
Assert.True(Unsafe.AreSame(ref stringRef, ref MemoryMarshal.GetReference(listSpan)));
203+
}
204+
205+
private static void SequenceEqual<T>(Span<T> span, Span<T> expected)
206+
{
207+
Assert.AreEqual(expected.Length, span.Length);
208+
for (var i = 0; i < expected.Length; i++)
209+
{
210+
Assert.AreEqual(expected[i], span[i]);
211+
}
212+
}
213+
214+
private class IntAsObject
215+
{
216+
public int Value;
217+
}
218+
}

Diff for: csharp/src/Google.Protobuf/Collections/RepeatedField.cs

+36
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
using System.IO;
1515
using System.Linq;
1616
using System.Security;
17+
#if NET5_0_OR_GREATER
18+
using System.Runtime.CompilerServices;
19+
#endif
1720

1821
namespace Google.Protobuf.Collections
1922
{
@@ -643,6 +646,39 @@ public T this[int index]
643646
}
644647
}
645648

649+
[SecuritySafeCritical]
650+
internal Span<T> AsSpan() => array.AsSpan(0, count);
651+
652+
internal void SetCount(int targetCount)
653+
{
654+
if (targetCount < 0)
655+
{
656+
throw new ArgumentOutOfRangeException(
657+
nameof(targetCount),
658+
targetCount,
659+
"Non-negative number required.");
660+
}
661+
662+
if (targetCount > Capacity)
663+
{
664+
EnsureSize(targetCount);
665+
}
666+
#if NET5_0_OR_GREATER
667+
else if (targetCount < count && RuntimeHelpers.IsReferenceOrContainsReferences<T>())
668+
{
669+
// Only reference types need to be cleared to allow GC to collect them.
670+
Array.Clear(array, targetCount, count - targetCount);
671+
}
672+
#else
673+
else if (targetCount < count)
674+
{
675+
Array.Clear(array, targetCount, count - targetCount);
676+
}
677+
#endif
678+
679+
count = targetCount;
680+
}
681+
646682
#region Explicit interface implementation for IList and ICollection.
647683
bool IList.IsFixedSize => false;
648684

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#region Copyright notice and license
2+
3+
// Protocol Buffers - Google's data interchange format
4+
// Copyright 2008 Google Inc. All rights reserved.
5+
//
6+
// Use of this source code is governed by a BSD-style
7+
// license that can be found in the LICENSE file or at
8+
// https://developers.google.com/open-source/licenses/bsd
9+
10+
#endregion
11+
12+
using System;
13+
using System.Security;
14+
using Google.Protobuf.Collections;
15+
16+
namespace Google.Protobuf;
17+
18+
/// <summary>
19+
/// An unsafe class that provides a set of methods to access the underlying data representations of
20+
/// collections.
21+
/// </summary>
22+
[SecuritySafeCritical]
23+
public static class UnsafeCollectionOperations
24+
{
25+
/// <summary>
26+
/// <para>
27+
/// Returns a <see cref="Span{T}"/> that wraps the current backing array of the given
28+
/// <see cref="RepeatedField{T}"/>.
29+
/// </para>
30+
/// <para>
31+
/// Values in the <see cref="Span{T}"/> should not be set to null. Use
32+
/// <see cref="RepeatedField{T}.Remove(T)"/> or <see cref="RepeatedField{T}.RemoveAt(int)"/> to
33+
/// remove items instead.
34+
/// </para>
35+
/// <para>
36+
/// The returned <see cref="Span{T}"/> is only valid until the size of the
37+
/// <see cref="RepeatedField{T}"/> is modified, after which its state becomes undefined.
38+
/// Modifying existing elements without changing the size is safe as long as the modifications
39+
/// do not set null values.
40+
/// </para>
41+
/// </summary>
42+
/// <typeparam name="T">
43+
/// The type of elements in the <see cref="RepeatedField{T}"/>.
44+
/// </typeparam>
45+
/// <param name="field">
46+
/// The <see cref="RepeatedField{T}"/> for which to wrap the current backing array. Must not be
47+
/// null.
48+
/// </param>
49+
/// <returns>
50+
/// A <see cref="Span{T}"/> that wraps the current backing array of the
51+
/// <see cref="RepeatedField{T}"/>.
52+
/// </returns>
53+
/// <exception cref="ArgumentNullException">
54+
/// Thrown if <paramref name="field"/> is <see langword="null"/>.
55+
/// </exception>
56+
public static Span<T> AsSpan<T>(RepeatedField<T> field)
57+
{
58+
ProtoPreconditions.CheckNotNull(field, nameof(field));
59+
return field.AsSpan();
60+
}
61+
62+
/// <summary>
63+
/// <para>
64+
/// Sets the count of the specified <see cref="RepeatedField{T}"/> to the given value.
65+
/// </para>
66+
/// <para>
67+
/// This method should only be called if the subsequent code guarantees to populate
68+
/// the field with the specified number of items.
69+
/// </para>
70+
/// <para>
71+
/// If count is less than <see cref="RepeatedField{T}.Count"/>, the collection is effectively
72+
/// trimmed down to the first count elements. <see cref="RepeatedField{T}.Capacity"/>
73+
/// is unchanged, meaning the underlying array remains allocated.
74+
/// </para>
75+
/// </summary>
76+
/// <typeparam name="T">
77+
/// The type of elements in the <see cref="RepeatedField{T}"/>.
78+
/// </typeparam>
79+
/// <param name="field">
80+
/// The field to set the count of. Must not be null.
81+
/// </param>
82+
/// <param name="count">
83+
/// The value to set the field's count to. Must be non-negative.
84+
/// </param>
85+
/// <exception cref="ArgumentNullException">
86+
/// Thrown if <paramref name="field"/> is <see langword="null"/>.
87+
/// </exception>
88+
/// <exception cref="ArgumentOutOfRangeException">
89+
/// Thrown if <paramref name="count"/> is negative.
90+
/// </exception>
91+
public static void SetCount<T>(RepeatedField<T> field, int count)
92+
{
93+
ProtoPreconditions.CheckNotNull(field, nameof(field));
94+
field.SetCount(count);
95+
}
96+
}

0 commit comments

Comments
 (0)