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 }