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