1 /** Extensions to std.datetime.benchmark.
2 
3 	Copyright: Per Nordlöw 2022-.
4 	License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
5 	Authors: $(WEB Per Nordlöw)
6 
7 	TODO: Use ggplot or similar to visualize results.
8 	TODO: Use `nxt.sampling` to generate test data.
9 */
10 module nxt.benchmark;
11 
12 /** Behavior or reservation of space for a specific implicit length/size/count.
13  */
14 enum ReserveSupport {
15 	no,							///< Reservation is not (yet) supported.
16 	yes,						///< Reservation is supported.
17 	always,	///< Reservation is not needed because of unconditional pre-allocation, either static or dynamic.
18 }
19 
20 struct Results {
21 	import core.time : Duration;
22 	@property void toString(Sink)(ref scope Sink sink) const {
23 		import std.format : formattedWrite;
24 		foreach (memberName; __traits(allMembers, typeof(this))) {
25 			const member = __traits(getMember, this, memberName);
26 			alias Member = typeof(member);
27 			static if (is(immutable Member == immutable Duration)) {
28 				if (member == Member.init)
29 					continue;
30 				sink.formattedWrite("%s: %s ns/op",
31 									memberName,
32 									cast(double)(member).total!"nsecs" / elementCount);
33 			}
34 		}
35 	}
36 	string typeName;
37 	size_t elementCount;
38 	size_t runCount;
39 	private Duration _insertWithoutGrowthTime;
40 	private Duration _insertWithGrowthTime;
41 	private Duration _removeTime;
42 	private Duration _containsTime;
43 	private Duration _inTime;
44 	private Duration _rehashTime;
45 	private Duration _inAfterRehashTime;
46 	private Duration _indexTime;
47 	private Duration _appendTime;
48 }
49 
50 /// Number of run per benchmark.
51 debug
52 	static immutable runCountDefault = 3; // lighter test in debug mode
53 else
54 	static immutable runCountDefault = 10;
55 
56 /// Formatting uses some extra space but should be removed when outputting to plots.
57 static immutable formatNsPerOp = "%6.1f ns/op";
58 
59 /** Benchmark append operation available in type `A` with test source `S`.
60  */
61 Results benchmarkAppendable(A, Sample, Source)(in Source testSource, in size_t runCount = runCountDefault) {
62 	import core.time : Duration;
63 	import std.datetime : MonoTime;
64 	import std.conv : to;
65 	import std.algorithm.searching : minElement, maxElement;
66 	import std.stdio : writef, writefln;
67 
68 	ReserveSupport reserveSupport;
69 	auto results = typeof(return)(A.stringof, testSource.length, runCount);
70 
71 	writef("- ");
72 
73 	scope spans = new Duration[runCount];
74 
75 	A _ = makeWithRequestedCapacity!(A)(0, reserveSupport);
76 	foreach (const runIx; 0 .. runCount) {
77 		A a = makeWithRequestedCapacity!(A)(results.elementCount, reserveSupport);
78 		const start = MonoTime.currTime();
79 		foreach (immutable i; testSource) {
80 			static	  if (is(typeof(a ~= i)))
81 				a ~= i;
82 			else static if (is(typeof(a ~= i.to!Sample)))
83 				a ~= i.to!Sample;
84 			else static if (is(typeof(a.put(i))))
85 				a.put(i);
86 			else static if (is(typeof(a.put(i.to!Sample))))
87 				a.put(i.to!Sample);
88 			else
89 				static assert(0, "Cannot append a `" ~ typeof(i).stringof ~ "` to a `" ~ A.stringof ~ "`");
90 		}
91 		spans[runIx] = MonoTime.currTime() - start;
92 		static if (__traits(hasMember, A, `clear`) &&
93 				   is(typeof(a.clear())))
94 			a.clear();
95 	}
96 	results._appendTime = spans[].minElement();
97 	writef("append (%s) (~=): "~formatNsPerOp,
98 		   reserveSupport == ReserveSupport.no ? "no growth" : "with growth",
99 		   cast(double)(results._appendTime).total!"nsecs" / results.elementCount);
100 
101 	writefln(` for %s`, A.stringof);
102 
103 	return results;
104 }
105 
106 /** Benchmark set (container) type `A` with test source `S`.
107  */
108 Results benchmarkSet(A, Sample, Source)(in Source testSource, in size_t runCount = runCountDefault)
109 /+ TODO: if (isSet!A) +/
110 {
111 	import core.time : Duration;
112 	import std.datetime : MonoTime;
113 	import std.conv : to;
114 	import std.stdio : writef, writefln, writeln;
115 	import std.algorithm.searching : minElement, maxElement;
116 	import nxt.address : AlignedAddress;
117 
118 	alias Address = AlignedAddress!1;
119 
120 	ReserveSupport reserveSupport;
121 	scope spans = new Duration[runCount];
122 	auto results = typeof(return)(A.stringof, testSource.length, runCount);
123 
124 	writef("- ");
125 
126 	A a;
127 	{							// needs scope
128 		foreach (const runIx; 0 .. runCount) {
129 			const start = MonoTime.currTime();
130 			foreach (immutable i; testSource) {
131 				static if (__traits(hasMember, A, `ElementType`) &&
132 						   is(A.ElementType == ubyte[]))
133 					a.insert(i.toUbytes);
134 				else {
135 					static if (__traits(hasMember, A, `ElementType`)) {
136 						static if (is(A.ElementType == Address))
137 							const element = A.ElementType(i + 1); ///< Start at 1 instead of 0 because `Address` uses 0 for `nullValue`.
138 						else static if (is(A.ElementType : string) ||
139 										is(typeof(A.ElementType(string.init))))
140 							const element = A.ElementType(to!string(i));
141 						else
142 							const element = A.ElementType(i);
143 					}
144 					else
145 						const element = i;
146 					a.insert(element);
147 				}
148 			}
149 			spans[runIx] = MonoTime.currTime() - start;
150 			static if (__traits(hasMember, A, `clear`) &&
151 					   is(typeof(a.clear())))
152 				if (runIx+1 != runCount)
153 					a.clear();	// clear all but the last one needed in contains below
154 		}
155 		results._insertWithGrowthTime = spans[].minElement();
156 		writef("insert (with growth): "~formatNsPerOp,
157 			   cast(double)(results._insertWithGrowthTime).total!"nsecs" / results.elementCount);
158 	}
159 
160 	{							// needs scope
161 		const start = MonoTime.currTime();
162 		size_t hitCount = 0;
163 		foreach (immutable i; testSource) {
164 			static if (__traits(hasMember, A, `ElementType`) &&
165 					   is(A.ElementType == ubyte[]))
166 				hitCount += a.contains(i.toUbytes);
167 			else {
168 				static if (__traits(hasMember, A, `ElementType`)) {
169 					static if (is(A.ElementType == Address))
170 						const element = A.ElementType(i + 1); ///< Start at 1 instead of 0 because `Address` uses 0 for `nullValue`.
171 					else static if (is(A.ElementType : string) ||
172 									is(typeof(A.ElementType(string.init))))
173 						const element = A.ElementType(to!string(i));
174 					else
175 						const element = A.ElementType(i); // wrap in `i` in `Nullable`
176 				}
177 				else
178 					const element = i;
179 				static if (__traits(hasMember, A, "contains"))
180 					hitCount += a.contains(element);
181 				else static if (is(typeof(a.contains(element)) == bool))
182 					hitCount += a.contains(element);
183 				else static if (is(typeof(element in a) == bool))
184 					hitCount += element in a;
185 				else
186 					static assert(0,
187 								  "Cannot check that " ~
188 								  typeof(a).stringof ~
189 								  " contains " ~
190 								  typeof(element).stringof);
191 			}
192 		}
193 		const ok = hitCount == results.elementCount; // for side effect in output
194 		results._containsTime = MonoTime.currTime() - start;
195 		writef(", contains: "~formatNsPerOp~" ns/op (%s)",
196 			   cast(double)(results._containsTime).total!"nsecs" / results.elementCount,
197 			   ok ? "OK" : "ERR");
198 	}
199 
200 	const _ = makeWithRequestedCapacity!(A)(0, reserveSupport);
201 	if (reserveSupport) {
202 		foreach (const runIx; 0 .. runCount) {
203 			A b = makeWithRequestedCapacity!(A)(results.elementCount, reserveSupport);
204 			const start = MonoTime.currTime();
205 			foreach (immutable i; testSource) {
206 				static if (__traits(hasMember, A, `ElementType`) &&
207 						   is(A.ElementType == ubyte[]))
208 					b.insert(i.toUbytes);
209 				else {
210 					static if (__traits(hasMember, A, `ElementType`)) {
211 						static if (is(A.ElementType == Address))
212 							const element = A.ElementType(i + 1); ///< Start at 1 instead of 0 because `Address` uses 0 for `nullValue`.
213 						else static if (is(A.ElementType : string) ||
214 										is(typeof(A.ElementType(string.init))))
215 							const element = A.ElementType(to!string(i));
216 						else
217 							const element = A.ElementType(i); // wrap in `i` in `Nullable`
218 					}
219 					else
220 						const element = i;
221 					b.insert(element);
222 				}
223 			}
224 			spans[runIx] = MonoTime.currTime() - start;
225 			static if (__traits(hasMember, A, `clear`) &&
226 					   is(typeof(b.clear())))
227 				b.clear();		  // TODO why does this fail for `RadixTreeMap`?
228 
229 		}
230 		results._insertWithoutGrowthTime = spans[].minElement();
231 		writef(", insert (no growth): "~formatNsPerOp,
232 			   cast(double)(results._insertWithoutGrowthTime).total!"nsecs" / results.elementCount);
233 	}
234 
235 	writef(` for %s`, A.stringof);
236 
237 	static if (__traits(hasMember, A, `binCounts`))
238 		writef(" %s", a.binCounts());
239 	static if (__traits(hasMember, A, `smallBinCapacity`))
240 		writef(" smallBinCapacity:%s", A.smallBinCapacity);
241 	static if (__traits(hasMember, A, `averageProbeCount`))
242 		writef(" averageProbeCount:%s", a.averageProbeCount);
243 
244 	writeln();
245 
246 	return results;
247 }
248 
249 /** Benchmark map (container) type `A` with test source `S`.
250  */
251 Results benchmarkMap(A, Sample, Source)(in Source testSource, in size_t runCount = runCountDefault)
252 /+ TODO: if (isMap!A || __traits(isAssociativeArray, A)) +/
253 {
254 	import core.time : Duration;
255 	import std.datetime : MonoTime;
256 	import std.conv : to;
257 	import std.stdio : writef, writefln, writeln;
258 	import std.algorithm.searching : minElement, maxElement;
259 	import nxt.address : AlignedAddress;
260 
261 	alias Address = AlignedAddress!1;
262 
263 	ReserveSupport reserveSupport;
264 	scope spans = new Duration[runCount];
265 	auto results = typeof(return)(A.stringof, testSource.length, runCount);
266 
267 	writef("- ");
268 
269 	// determine key and value type. TODO: extract to trait `KeyValueType(A)`
270 	static if (is(A : V[K], K, V)) {
271 		alias KeyType = K;
272 		alias ValueType = K;
273 	} else {
274 		// determine key type. TODO: extract to trait `KeyType(A)`
275 		static if (is(A.KeyType)) {
276 			alias KeyType = A.KeyType;
277 		} else static if (is(typeof(A.KeyValue.key))) { // StringMap
278 			alias KeyType = typeof(A.KeyValue.key);
279 		} else static if (is(immutable typeof(A.keys()) == immutable K[], K)) { // emsi HashMap
280 			alias KeyType = K;
281 		} else static if (__traits(hasMember, A, "byKey")) { // emsi HashMap
282 			import std.range.primitives : std_ElementType = ElementType;
283 			alias KeyType = std_ElementType!(typeof(A.init.byKey()));
284 		} else {
285 			static assert(0, "Could not determine key type of " ~ A.stringof ~ " " ~ typeof(A.keys()).stringof);
286 		}
287 		// determine value type. TODO: extract to trait `ValueType(A)`
288 		static if (is(A.ValueType)) {
289 			alias ValueType = A.ValueType;
290 		} else static if (is(typeof(A.KeyValue.value))) { // StringMap
291 			alias ValueType = typeof(A.KeyValue.value);
292 		} else static if (__traits(hasMember, A, "byValue")) { // emsi HashMap
293 			import std.range.primitives : std_ElementType = ElementType;
294 			alias ValueType = std_ElementType!(typeof(A.init.byValue()));
295 		} else {
296 			static assert(0, "Could not determine value type of " ~ A.stringof);
297 		}
298 	}
299 
300 	// determine element type. TODO: extract to trait `ElementType(A)` or reuse `std.primitives.ElementType`
301 	static if (is(A.ElementType)) {
302 		alias ElementType = A.ElementType;
303 	} else static if (is(A.KeyValue)) { // StringMap
304 		alias ElementType = A.KeyValue;
305 	} else static if (__traits(hasMember, A, "byKeyValue") || // emsi HashMap
306 					  !is(typeof(A.init.byKeyValue) == void)) {
307 		import std.range.primitives : std_ElementType = ElementType;
308 		alias ElementType = std_ElementType!(typeof(A.init.byKeyValue()));
309 	} else {
310 		static assert(0, "Could not determine element type of " ~ A.stringof);
311 	}
312 
313 	// allocate
314 	const keys = iotaArrayOf!(KeyType)(0, results.elementCount);
315 
316 	A a;
317 
318 	// insert/opIndex without preallocation via void capacity(size_t)
319 	{
320 		foreach (const runIx; 0 .. runCount) {
321 			const start = MonoTime.currTime();
322 			foreach (immutable i; testSource) {
323 				static if (is(typeof(A.init.insert(ElementType.init)))) {
324 					static if (is(KeyType == Address))
325 						const element = ElementType(Address(keys[i] + 1), // avoid `Address.null Value`
326 													ValueType.init);
327 					else
328 						const element = ElementType(keys[i], ValueType.init);
329 					a.insert(element);
330 				}
331 				else
332 					a[keys[i]] = ValueType.init; // AAs
333 			}
334 			spans[runIx] = MonoTime.currTime() - start;
335 		}
336 		results._insertWithGrowthTime = spans[].minElement();
337 		writef("insert (with growth): "~formatNsPerOp,
338 			   cast(double)(results._insertWithGrowthTime).total!"nsecs" / results.elementCount);
339 	}
340 
341 	// contains
342 	static if (is(typeof(A.init.contains(KeyType.init)) : bool)) {
343 		{
344 			bool okAll = true;
345 			foreach (const runIx; 0 .. runCount) {
346 				const start = MonoTime.currTime();
347 				size_t hitCount = 0;
348 				foreach (immutable i; testSource) {
349 					static if (is(KeyType == Address))
350 						hitCount += a.contains(Address(keys[i] + 1)); // avoid `Address.nullValue`
351 					else
352 						hitCount += a.contains(keys[i]) ? 1 : 0;
353 				}
354 				const ok = hitCount == results.elementCount; // for side effect in output
355 				if (!ok)
356 					okAll = false;
357 				spans[runIx] = MonoTime.currTime() - start;
358 			}
359 			results._containsTime = spans[].minElement();
360 			writef(", contains: "~formatNsPerOp~" ns/op (%s)",
361 				   cast(double)(results._containsTime).total!"nsecs" / results.elementCount,
362 				   okAll ? "OK" : "ERR");
363 		}
364 	}
365 
366 	// in
367 	{
368 		bool okAll = true;
369 		foreach (const runIx; 0 .. runCount) {
370 			const start = MonoTime.currTime();
371 			size_t hitCount = 0;
372 			foreach (immutable i; testSource) {
373 				static if (is(KeyType == Address))
374 					hitCount += cast(bool)(Address(keys[i] + 1) in a); // avoid `Address.nullValue`
375 				else
376 					hitCount += cast(bool)(keys[i] in a);
377 			}
378 			const ok = hitCount == results.elementCount; // for side effect in output
379 			if (!ok)
380 				okAll = false;
381 			spans[runIx] = MonoTime.currTime() - start;
382 		}
383 		results._inTime = spans[].minElement();
384 		writef(",	   in: "~formatNsPerOp~" ns/op (%s)",
385 			   cast(double)(results._inTime).total!"nsecs" / results.elementCount,
386 			   okAll ? "OK" : "ERR");
387 	}
388 
389 	// rehash (AAs) + in
390 	static if (is(typeof(a.rehash()))) {
391 		// rehash
392 		foreach (const runIx; 0 .. runCount) {
393 			const start = MonoTime.currTime();
394 			a.rehash();
395 			spans[runIx] = MonoTime.currTime() - start;
396 		}
397 		results._rehashTime = spans[].minElement();
398 		writef(", rehash: "~formatNsPerOp,
399 			   cast(double)(results._rehashTime).total!"nsecs" / results.elementCount);
400 		// in
401 		foreach (const runIx; 0 .. runCount) {
402 			const start = MonoTime.currTime();
403 			foreach (immutable i; testSource)
404 				const hit = keys[i] in a;
405 			spans[runIx] = MonoTime.currTime() - start;
406 		}
407 		results._inAfterRehashTime = spans[].minElement();
408 		writef(", in (after rehash): "~formatNsPerOp,
409 			   cast(double)(results._inAfterRehashTime).total!"nsecs" / results.elementCount);
410 	}
411 
412 	A _ = makeWithRequestedCapacity!(A)(0, reserveSupport);
413 	if (reserveSupport) {
414 		bool unsupported = false;
415 		// insert/opIndex with preallocation via void capacity(size_t)
416 		foreach (const runIx; 0 .. runCount) {
417 			A b = makeWithRequestedCapacity!(A)(results.elementCount, reserveSupport);
418 			const start = MonoTime.currTime();
419 			foreach (immutable i; testSource) {
420 				static if (__traits(hasMember, A, "insert")) {
421 					static if (is(immutable A.KeyType == immutable Address))
422 						const key = Address(keys[i] + 1); // avoid `Address.nullValue`
423 					else
424 						const key = keys[i];
425 					static if (is(typeof(b.insert(key, ValueType.init))))
426 						b.insert(key, ValueType.init);
427 					else static if (is(typeof(b.insert(ElementType(key, ValueType.init)))))
428 						b.insert(ElementType(key, ValueType.init));
429 					else
430 					{
431 						pragma(msg, "Skipping unsupported insert for " ~ A.stringof);
432 						unsupported = true;
433 					}
434 				}
435 				else
436 					b[keys[i]] = ValueType.init;
437 			}
438 			spans[runIx] = MonoTime.currTime() - start;
439 			static if (__traits(hasMember, A, `clear`) &&
440 					   is(typeof(b.clear())))
441 				b.clear();
442 		}
443 		if (!unsupported) {
444 			results._insertWithoutGrowthTime = spans[].minElement();
445 			writef(", insert (no growth): "~formatNsPerOp,
446 				   cast(double)(results._insertWithoutGrowthTime).total!"nsecs" / results.elementCount);
447 		}
448 	}
449 
450 	writef(` for %s`, A.stringof);
451 
452 	static if (__traits(hasMember, A, `binCounts`))
453 		writef(" %s", a.binCounts());
454 	static if (__traits(hasMember, A, `smallBinCapacity`))
455 		writef(" smallBinCapacity:%s", A.smallBinCapacity);
456 	static if (__traits(hasMember, A, `totalProbeCount`))
457 		writef(" averageProbeCount:%s", cast(double)a.totalProbeCount/a.length);
458 
459 	writeln();
460 
461 	return results;
462 }
463 
464 alias benchmarkAssociativeArray = benchmarkMap;
465 
466 /** Make `A` and try setting its capacity to `results.elementCount`.
467  */
468 auto makeWithRequestedCapacity(A)(size_t elementCount, out ReserveSupport reserveSupport) {
469 	static if (is(typeof(A.init.withCapacity(elementCount)))) {
470 		reserveSupport = ReserveSupport.yes;
471 		return A.withCapacity(elementCount);
472 	}
473 	else
474 	{
475 		static if (is(A == class))
476 			auto a = new A();
477 		else static if (is(A == struct))
478 			auto a = A();
479 		else
480 			A a;
481 
482 		static if (__traits(hasMember, A, `reserve`)) {
483 			static if (__traits(compiles, { a.reserve(elementCount); })) {
484 				a.reserve(elementCount);
485 				reserveSupport = ReserveSupport.yes;
486 			}
487 			else static if (__traits(compiles, { a.reserve!uint(elementCount); })) {
488 				a.reserve!uint(elementCount);
489 				reserveSupport = ReserveSupport.yes;
490 			}
491 		}
492 		else static if (is(A == U[], U)) {
493 			// this doesn’t work aswell:
494 			// import std.range.primitives : ElementType;
495 			// return new ElementType!A[elementCount];
496 			a.reserve(elementCount); // See_Also: https://dlang.org/library/object/reserve.html
497 			reserveSupport = ReserveSupport.yes;
498 			return a;
499 		}
500 		else static if (is(A : V[K], K, V)) {
501 			static if (__traits(compiles, { a.reserve(elementCount); })) {
502 				a.reserve(elementCount); // builtin AA’s might get this later on
503 				reserveSupport = ReserveSupport.yes;
504 			}
505 		}
506 		else static if (is(typeof(a.reserve(elementCount)))) {
507 			a.reserve(elementCount);
508 			reserveSupport = ReserveSupport.yes;
509 		}
510 
511 		static if (__traits(isPOD, A))
512 			return a;
513 		else
514 		{
515 			import std.algorithm.mutation : move;
516 			return move(a);
517 		}
518 	}
519 }
520 
521 private T[] iotaArrayOf(T)(in size_t begin, in size_t end) {
522 	import std.typecons : Nullable;
523 
524 	typeof(return) es = new T[end];
525 	foreach (immutable i; begin .. end) {
526 		static if (is(typeof(T(i)))) // if possible
527 			es[i] = T(i);	   // try normal construction
528 		else {
529 			import std.conv : to;
530 			static if (is(typeof(T(i))))
531 				es[i] = T(i);
532 			else static if (is(typeof(T(i.to!string))))
533 				es[i] = T(i.to!string);
534 			else static if (is(typeof(i.to!T)))
535 				es[i] = i.to!T;
536 			else static if (is(T == Nullable!(uint, uint.max))) /+ TODO: avoid this +/
537 				es[i] = T(cast(uint)i);
538 			else
539 				static assert(0, "Cannot convert `" ~ typeof(i).stringof ~ "` to `" ~ T.stringof ~ "`");
540 		}
541 	}
542 	return es;
543 }