Skip to content

Commit 9404851

Browse files
authored
Add support for decimals in ValuesGenerator. (dotnet#2547)
* Add support for decimals in ValuesGenerator. As suggested in the documentation, added support for the decimal type. Also corrected the documentation to properly enumerate the supported types. Fixes dotnet#2546 * Add tests to cover supported types in ValueGenerator Also required updates to ValueGenerator.GenerateValue method to support byte * Fix compilation errors in AlignedMemory On newer platforms, the NativeMemory.AlignedAlloc wants UIntPtr not uint arguments * Addressed review comments Added a lot of documentation about what public methods do. Added exceptions when invalid requests of the unique methods for too many distinct values. Switched to single-line methods for SupportsXXX Added notes on why we cannot change the existing behavior of supported types, and what the internal methods do (range and values returned). Added testing coverage for new exceptions * Fixed documentation Failed to call out sbyte support in ValuesGenerator
1 parent 3e63b12 commit 9404851

File tree

4 files changed

+205
-22
lines changed

4 files changed

+205
-22
lines changed

docs/microbenchmark-design-guidelines.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ T[] Array<T>(int count);
368368
Dictionary<TKey, TValue> Dictionary<TKey, TValue>(int count)
369369
```
370370

371-
As of today, the `T` can be: `byte`, `char`, `int`, `double`, `bool` and `string`. Extending `T` to more types is very welcomed!
371+
As of today, the `T` can be: `byte`, `sbyte`, `char`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `bool`, `decimal`, `string`, and `Guid`. Extending `T` to more types is very welcomed!
372372

373373
**Note:** `ValuesGenerator` is simply always creating a new instance of `Random` with a constant seed. It's a crucial component and its correctness is verified using [Unit Tests](https://github.com/dotnet/performance/blob/main/src/tests/harness/BenchmarkDotNet.Extensions.Tests/UniqueValuesGeneratorTests.cs).
374374

src/benchmarks/micro/libraries/Common/AlignedMemory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ private unsafe AlignedMemory(void* memory, int length)
2424
}
2525

2626
public static unsafe AlignedMemory Allocate(uint length, uint alignment)
27-
=> new AlignedMemory(NativeMemory.AlignedAlloc(length, alignment), (int)length);
27+
=> new AlignedMemory(NativeMemory.AlignedAlloc((UIntPtr)length, (UIntPtr)alignment), (int)length);
2828

2929
public bool IsDisposed => _disposed;
3030

src/harness/BenchmarkDotNet.Extensions/ValuesGenerator.cs

Lines changed: 109 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,38 @@ public static class ValuesGenerator
1414
{
1515
private const int Seed = 12345; // we always use the same seed to have repeatable results!
1616

17+
/// <summary>
18+
/// Returns a T value that is NOT the default(T) (typically zero) value.
19+
/// For bool, there's only one choice (true/1).
20+
/// For byte/sbyte, will never return byte.MaxValue (256) so there are a maximum of 254 values.
21+
/// </summary>
1722
public static T GetNonDefaultValue<T>()
1823
{
19-
if (typeof(T) == typeof(byte)) // we can't use ArrayOfUniqueValues for byte
20-
return Array<T>(byte.MaxValue).First(value => !value.Equals(default));
24+
if (typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte)) // we can't use ArrayOfUniqueValues for byte/sbyte (but they have the same range)
25+
return Array<T>(byte.MaxValue).First(value => !value.Equals(default(T)));
2126
else
22-
return ArrayOfUniqueValues<T>(2).First(value => !value.Equals(default));
27+
return ArrayOfUniqueValues<T>(2).First(value => !value.Equals(default(T)));
2328
}
2429

2530
/// <summary>
26-
/// does not support byte because there are only 256 unique byte values
31+
/// Returns an array of the requested size where each entry is a distinct value.
32+
/// For byte and sbyte support a maximum count of 255 because there are only 256
33+
/// unique byte values. For bool, there are only 2 values so throw for more requested
2734
/// </summary>
2835
public static T[] ArrayOfUniqueValues<T>(int count)
2936
{
37+
if (count > 2 && typeof(T) == typeof(bool))
38+
throw new ArgumentOutOfRangeException("count", "Cannot exceed 2 for bool values");
39+
if (count > 255 && (typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte)))
40+
throw new ArgumentOutOfRangeException("count", "Cannot exceed 255 for byte or sbyte values");
41+
3042
// allocate the array first to try to take advantage of memory randomization
3143
// as it's usually the first thing called from GlobalSetup method
3244
// which with MemoryRandomization enabled is the first method called right after allocation
3345
// of random-sized memory by BDN engine
3446
T[] result = new T[count];
3547

36-
var random = new Random(Seed);
48+
var random = new Random(Seed);
3749

3850
var uniqueValues = new HashSet<T>();
3951

@@ -49,12 +61,17 @@ public static T[] ArrayOfUniqueValues<T>(int count)
4961

5062
return result;
5163
}
52-
64+
65+
/// <summary>
66+
/// Returns an array of the requested size where each entry is a random value and values could repeat
67+
/// For byte and sbyte values are built from Random.NextBytes, for other types GenerateValue is used
68+
/// to generate a random value in the appropriate range
69+
/// </summary>
5370
public static T[] Array<T>(int count)
5471
{
5572
var result = new T[count];
5673

57-
var random = new Random(Seed);
74+
var random = new Random(Seed);
5875

5976
if (typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte))
6077
{
@@ -82,6 +99,10 @@ public static T[] Array<T>(int count)
8299
52, 53, 54, 55, 56, 57, 43, 47 //4..9, +, /
83100
};
84101

102+
/// <summary>
103+
/// Returns an array of bytes of the requested size where each entry is a random value
104+
/// from the legal subset of ASCII characters used in Base-64 encoded values
105+
/// </summary>
85106
public static byte[] ArrayBase64EncodingBytes(int count)
86107
{
87108
var result = new byte[count];
@@ -97,8 +118,18 @@ public static byte[] ArrayBase64EncodingBytes(int count)
97118
return result;
98119
}
99120

121+
/// <summary>
122+
/// Returns a Dictionary of the requested size where each entry is a has a distinct key value and
123+
/// the stored values are randomly generated.
124+
/// GenerateValue is used to generate a random value in the appropriate range for both the key and value
125+
/// </summary>
100126
public static Dictionary<TKey, TValue> Dictionary<TKey, TValue>(int count)
101127
{
128+
if (count > 2 && typeof(TKey) == typeof(bool))
129+
throw new ArgumentOutOfRangeException("count", "Cannot exceed 2 for Dictionary<bool, TValue>");
130+
if (count > 255 && (typeof(TKey) == typeof(byte) || typeof(TKey) == typeof(sbyte)))
131+
throw new ArgumentOutOfRangeException("count", "Cannot exceed 255 for Dictionary<byte, TValue> or Dictionary<sbyte, TValue>");
132+
102133
var dictionary = new Dictionary<TKey, TValue>();
103134

104135
var random = new Random(Seed);
@@ -114,6 +145,11 @@ public static Dictionary<TKey, TValue> Dictionary<TKey, TValue>(int count)
114145
return dictionary;
115146
}
116147

148+
/// <summary>
149+
/// Returns an array of the requested size where each entry is a random string value.
150+
/// The values are random length strings within the range requested and are composed
151+
/// a random assortment of the letters a..z, A..Z, and digits 0..9
152+
/// </summary>
117153
public static string[] ArrayOfStrings(int count, int minLength, int maxLength)
118154
{
119155
var random = new Random(Seed);
@@ -126,36 +162,52 @@ public static string[] ArrayOfStrings(int count, int minLength, int maxLength)
126162
return strings;
127163
}
128164

165+
/// <summary>
166+
/// Returns a random of the type requested
167+
/// For strings, it will be a random assortment of the letters 'a'..'z', 'A'..'Z', or '0'..'9' that is 1 to 50 characters long
168+
/// </summary>
129169
private static T GenerateValue<T>(Random random)
130170
{
171+
// Note: some of these types (especially the unsigned and values larger than int) are not giving the full range of values.
172+
// WE CANNOT change that now because existing performance tests are based on the values returned previously.
173+
if (typeof(T) == typeof(byte))
174+
return (T)(object)(byte)random.Next(byte.MinValue, byte.MaxValue); // note: will never return 256 as Random.Next max value is exclusive
175+
if (typeof(T) == typeof(sbyte))
176+
return (T)(object)(sbyte)random.Next(sbyte.MinValue, sbyte.MaxValue); // note: will never return 128 as Random.Next max value is exclusive
131177
if (typeof(T) == typeof(char))
132-
return (T)(object)(char)random.Next(char.MinValue, char.MaxValue);
178+
return (T)(object)(char)random.Next(char.MinValue, char.MaxValue); // note: will never return `\uffff` as Random.Next max value is exclusive
133179
if (typeof(T) == typeof(short))
134-
return (T)(object)(short)random.Next(short.MaxValue);
180+
return (T)(object)(short)random.Next(short.MaxValue); // note: will never return short.MaxValue as Random.Next max value is exclusive
135181
if (typeof(T) == typeof(ushort))
136-
return (T)(object)(ushort)random.Next(short.MaxValue);
182+
return (T)(object)(ushort)random.Next(short.MaxValue); // note: will never return short.MaxValue (right in the middle of the domain)
137183
if (typeof(T) == typeof(int))
138-
return (T)(object)random.Next();
184+
return (T)(object)random.Next(); // note: cannot call NextInt32 here, because that would change what we've returned in the past
139185
if (typeof(T) == typeof(uint))
140-
return (T)(object)(uint)random.Next();
186+
return (T)(object)(uint)random.Next(); // note: cannot call NextInt32 here, because that would change what we've returned in the past
141187
if (typeof(T) == typeof(long))
142-
return (T)(object)(long)random.Next();
188+
return (T)(object)(long)random.Next(); // note: will never return a value at or above int.MaxValue
143189
if (typeof(T) == typeof(ulong))
144-
return (T)(object)(ulong)random.Next();
190+
return (T)(object)(ulong)random.Next(); // note: will never return a value at or above int.MaxValue because Random.Next never returns negatives
145191
if (typeof(T) == typeof(float))
146192
return (T)(object)(float)random.NextDouble();
147193
if (typeof(T) == typeof(double))
148194
return (T)(object)random.NextDouble();
149195
if (typeof(T) == typeof(bool))
150196
return (T)(object)(random.NextDouble() > 0.5);
197+
if (typeof(T) == typeof(decimal))
198+
return (T)(object)GenerateRandomDecimal(random);
151199
if (typeof(T) == typeof(string))
152-
return (T)(object)GenerateRandomString(random, 1, 50);
200+
return (T)(object)GenerateRandomString(random, 1, 50); // note: all strings have only the characters 'a'..'z', 'A'..'Z', or '0'..'9'
153201
if (typeof(T) == typeof(Guid))
154-
return (T)(object)GenerateRandomGuid(random);
202+
return (T)(object)GenerateRandomGuid(random); // note: may return malformed Guids (not logically valid per RFC 4122 formatting)
155203

156204
throw new NotImplementedException($"{typeof(T).Name} is not implemented");
157205
}
158-
206+
207+
/// <summary>
208+
/// Returns a random length strings within the range requested and are composed
209+
/// a random assortment of the letters a..z, A..Z, and digits 0..9
210+
/// </summary>
159211
private static string GenerateRandomString(Random random, int minLength, int maxLength)
160212
{
161213
var length = random.Next(minLength, maxLength);
@@ -166,21 +218,58 @@ private static string GenerateRandomString(Random random, int minLength, int max
166218
var rangeSelector = random.Next(0, 3);
167219

168220
if (rangeSelector == 0)
169-
builder.Append((char) random.Next('a', 'z'));
221+
builder.Append((char)random.Next('a', 'z'));
170222
else if (rangeSelector == 1)
171-
builder.Append((char) random.Next('A', 'Z'));
223+
builder.Append((char)random.Next('A', 'Z'));
172224
else
173-
builder.Append((char) random.Next('0', '9'));
225+
builder.Append((char)random.Next('0', '9'));
174226
}
175227

176228
return builder.ToString();
177229
}
178230

231+
/// <summary>
232+
/// Returns a randomly generated Guid.
233+
/// Note: this is not guaranteed to be a reasonably formatted RFC 4122 Guid, NOR is this
234+
/// necessarily a "Version 4 Random" guid.
235+
/// </summary>
179236
private static Guid GenerateRandomGuid(Random random)
180237
{
181238
byte[] bytes = new byte[16];
182239
random.NextBytes(bytes);
183240
return new Guid(bytes);
184241
}
242+
243+
/// <summary>
244+
/// Returns a randomly generated decimal value.
245+
/// Note: returns a value across the entire valid decimal value-space
246+
/// </summary>
247+
private static decimal GenerateRandomDecimal(Random random)
248+
{
249+
// Decimal values have a sign, a scale, and 96 bits of significance (lo/mid/high)
250+
// generate those parts randomly and assemble a valid decimal
251+
byte scale = (byte)random.Next(29);
252+
bool sign = random.Next(2) == 0;
253+
return new decimal(random.NextInt32(),
254+
random.NextInt32(),
255+
random.NextInt32(),
256+
sign,
257+
scale);
258+
}
259+
260+
/// <summary>
261+
/// Returns a randomly generated int value from the entire range of legal 32-bit integers
262+
/// Note: using Random.Next will never return negative values so we can't use that where
263+
/// we want the complete bit-space.
264+
/// WARNING: Do not use this method in the GenerateValue for the various int-like types
265+
/// as that would change the range and order of values returned and THAT would change the
266+
/// performance benchmarks
267+
/// </summary>
268+
private static int NextInt32(this Random random)
269+
{
270+
int firstBits = random.Next(0, 1 << 4) << 28;
271+
int lastBits = random.Next(0, 1 << 28);
272+
return firstBits | lastBits;
273+
}
185274
}
186275
}

src/tests/harness/BenchmarkDotNet.Extensions.Tests/UniqueValuesGeneratorTests.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public void UnsupportedTypesThrow()
1818
[Fact]
1919
public void GeneratedArraysContainOnlyUniqueValues()
2020
{
21+
AssertGeneratedArraysContainOnlyUniqueValues<bool>(2);
22+
AssertGeneratedArraysContainOnlyUniqueValues<byte>(255);
2123
AssertGeneratedArraysContainOnlyUniqueValues<int>(1024);
2224
AssertGeneratedArraysContainOnlyUniqueValues<string>(1024);
2325
}
@@ -34,6 +36,7 @@ private void AssertGeneratedArraysContainOnlyUniqueValues<T>(int size)
3436
[Fact]
3537
public void GeneratedArraysContainAlwaysSameValues()
3638
{
39+
AssertGeneratedArraysContainAlwaysSameValues<byte>(255);
3740
AssertGeneratedArraysContainAlwaysSameValues<int>(1024);
3841
AssertGeneratedArraysContainAlwaysSameValues<string>(1024);
3942
}
@@ -68,9 +71,100 @@ public void GeneratedStringsContainOnlySimpleLettersAndDigits()
6871
[Fact]
6972
public void GetNonDefaultValueReturnsNonDefaultValue()
7073
{
74+
Assert.True(ValuesGenerator.GetNonDefaultValue<bool>());
7175
Assert.NotEqual(default(byte), ValuesGenerator.GetNonDefaultValue<byte>());
7276
Assert.NotEqual(default(int), ValuesGenerator.GetNonDefaultValue<int>());
7377
Assert.NotEqual(default(string), ValuesGenerator.GetNonDefaultValue<string>());
7478
}
79+
80+
[Fact]
81+
public void SupportsByte() => Supports<byte>(255); // note: testing to a maximum of 255 because byte.MaxValue is never generated
82+
83+
[Fact]
84+
public void ThrowsOnTooManyBytes() => Assert.Throws<ArgumentOutOfRangeException>(() => Supports<byte>(256));
85+
86+
[Fact]
87+
public void SupportsSignedByte() => Supports<sbyte>(127); // note: testing to a maximum of 127 because sbyte.MaxValue is never generated
88+
89+
[Fact]
90+
public void ThrowsOnTooManySignedBytes() => Assert.Throws<ArgumentOutOfRangeException>(() => Supports<sbyte>(256));
91+
92+
[Fact]
93+
public void SupportsChar() => Supports<char>();
94+
95+
[Fact]
96+
public void SupportsShort() => Supports<short>();
97+
98+
[Fact]
99+
public void SupportsUnsignedShort() => Supports<ushort>();
100+
101+
[Fact]
102+
public void SupportsInteger() => Supports<int>();
103+
104+
[Fact]
105+
public void SupportsUnsignedInteger() => Supports<uint>();
106+
107+
[Fact]
108+
public void SupportsLong() => Supports<long>();
109+
110+
[Fact]
111+
public void SupportsUnsignedLong() => Supports<ulong>();
112+
113+
[Fact]
114+
public void SupportsFloat() => Supports<float>();
115+
116+
[Fact]
117+
public void SupportsDouble() => Supports<double>();
118+
119+
[Fact]
120+
public void SupportsBool() => Supports<bool>(2);
121+
122+
[Fact]
123+
public void ThrowsOnTooManyBools() => Assert.Throws<ArgumentOutOfRangeException>(() => Supports<bool>(3));
124+
125+
[Fact]
126+
public void SupportsDecimal() => Supports<decimal>();
127+
128+
[Fact]
129+
public void SupportsString() => Supports<string>();
130+
131+
[Fact]
132+
public void SupportsGuid() => Supports<Guid>();
133+
134+
private static void SupportsArray<T>(int count)
135+
{
136+
var array = ValuesGenerator.Array<T>(count);
137+
Assert.NotNull(array);
138+
Assert.Equal(count, array.Length);
139+
}
140+
141+
private static void SupportsArrayOfUniques<T>(int count)
142+
{
143+
var array = ValuesGenerator.ArrayOfUniqueValues<T>(count);
144+
Assert.NotNull(array);
145+
Assert.Equal(count, array.Length);
146+
}
147+
148+
private static void SupportsNonDefaultValue<T>()
149+
{
150+
var value = ValuesGenerator.GetNonDefaultValue<T>();
151+
Assert.NotEqual(default, value);
152+
}
153+
154+
private static void SupportsDictionary<TKey, TValue>(int count)
155+
{
156+
var dictionary = ValuesGenerator.Dictionary<TKey, TValue>(count);
157+
Assert.NotNull(dictionary);
158+
Assert.Equal(count, dictionary.Count);
159+
}
160+
161+
private static void Supports<T>(int count = 10)
162+
{
163+
SupportsArray<T>(count);
164+
SupportsNonDefaultValue<T>();
165+
SupportsArrayOfUniques<T>(count);
166+
SupportsDictionary<T, T>(count);
167+
SupportsDictionary<T, string>(count);
168+
}
75169
}
76170
}

0 commit comments

Comments
 (0)