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 }