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