usearch 0.22.1 → 0.22.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -214,6 +214,11 @@ inline std::uint16_t f32_to_f16(float f32) noexcept {
214
214
  #endif
215
215
  }
216
216
 
217
+ /**
218
+ * @brief Numeric type for the IEEE 754 half-precision floating point.
219
+ * If hardware support isn't available, falls back to a hardware
220
+ * agnostic in-software implementation.
221
+ */
217
222
  class f16_bits_t {
218
223
  std::uint16_t uint16_{};
219
224
 
@@ -309,15 +314,31 @@ struct uuid_t {
309
314
  std::uint8_t octets[16];
310
315
  };
311
316
 
317
+ /**
318
+ * @brief An STL-based executor or a "thread-pool" for parallel execution.
319
+ * Isn't efficient for small batches, as it recreates the threads on every call.
320
+ */
312
321
  class executor_stl_t {
313
322
  std::size_t threads_count_{};
314
323
 
315
324
  public:
325
+ /**
326
+ * @param threads_count The number of threads to be used for parallel execution.
327
+ */
316
328
  executor_stl_t(std::size_t threads_count = 0) noexcept
317
329
  : threads_count_(threads_count ? threads_count : std::thread::hardware_concurrency()) {}
318
330
 
331
+ /**
332
+ * @return Maximum number of threads available to the executor.
333
+ */
319
334
  std::size_t size() const noexcept { return threads_count_; }
320
335
 
336
+ /**
337
+ * @brief Executes tasks in bulk using the specified thread-aware function.
338
+ * @param tasks The total number of tasks to be executed.
339
+ * @param thread_aware_function The thread-aware function to be called for each thread index and task index.
340
+ * @throws If an exception occurs during execution of the thread-aware function.
341
+ */
321
342
  template <typename thread_aware_function_at>
322
343
  void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept(false) {
323
344
  std::vector<std::thread> threads_pool;
@@ -333,6 +354,11 @@ class executor_stl_t {
333
354
  threads_pool[thread_idx].join();
334
355
  }
335
356
 
357
+ /**
358
+ * @brief Saturates every available thread with the given workload, until they finish.
359
+ * @param thread_aware_function The thread-aware function to be called for each thread index.
360
+ * @throws If an exception occurs during execution of the thread-aware function.
361
+ */
336
362
  template <typename thread_aware_function_at>
337
363
  void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept(false) {
338
364
  std::vector<std::thread> threads_pool;
@@ -345,14 +371,30 @@ class executor_stl_t {
345
371
 
346
372
  #if USEARCH_USE_OPENMP
347
373
 
374
+ /**
375
+ * @brief An OpenMP-based executor or a "thread-pool" for parallel execution.
376
+ * Is the preferred implementation, when available, and maximum performance is needed.
377
+ */
348
378
  class executor_openmp_t {
349
379
  public:
380
+ /**
381
+ * @param threads_count The number of threads to be used for parallel execution.
382
+ */
350
383
  executor_openmp_t(std::size_t threads_count = 0) noexcept {
351
384
  omp_set_num_threads(threads_count ? threads_count : std::thread::hardware_concurrency());
352
385
  }
353
386
 
387
+ /**
388
+ * @return Maximum number of threads available to the executor.
389
+ */
354
390
  std::size_t size() const noexcept { return omp_get_num_threads(); }
355
391
 
392
+ /**
393
+ * @brief Executes tasks in bulk using the specified thread-aware function.
394
+ * @param tasks The total number of tasks to be executed.
395
+ * @param thread_aware_function The thread-aware function to be called for each thread index and task index.
396
+ * @throws If an exception occurs during execution of the thread-aware function.
397
+ */
356
398
  template <typename thread_aware_function_at>
357
399
  void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept(false) {
358
400
  #pragma omp parallel for schedule(dynamic)
@@ -360,6 +402,11 @@ class executor_openmp_t {
360
402
  thread_aware_function(omp_get_thread_num(), i);
361
403
  }
362
404
 
405
+ /**
406
+ * @brief Saturates every available thread with the given workload, until they finish.
407
+ * @param thread_aware_function The thread-aware function to be called for each thread index.
408
+ * @throws If an exception occurs during execution of the thread-aware function.
409
+ */
363
410
  template <typename thread_aware_function_at>
364
411
  void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept(false) {
365
412
  #pragma omp parallel
@@ -415,18 +462,17 @@ class aligned_allocator_gt {
415
462
 
416
463
  using aligned_allocator_t = aligned_allocator_gt<>;
417
464
 
418
- #if !defined(USEARCH_DEFINED_WINDOWS)
419
-
420
465
  /**
421
466
  * @brief Memory-mapping allocator designed for "alloc many, free at once" usage patterns.
422
- * Thread-safe.
467
+ * Thread-safe, @b except constructors and destructors.
423
468
  *
424
469
  * Using this memory allocator won't affect your overall speed much, as that is not the bottleneck.
425
470
  * However, it can drastically improve memory usage especcially for huge indexes of small vectors.
426
471
  */
427
472
  template <std::size_t alignment_ak = 1> class memory_mapping_allocator_gt {
428
473
 
429
- static constexpr std::size_t min_size() { return 1024 * 1024 * 4; }
474
+ static constexpr std::size_t min_capacity() { return 1024 * 1024 * 4; }
475
+ static constexpr std::size_t capacity_multiplier() { return 2; }
430
476
  static constexpr std::size_t head_size() {
431
477
  /// Pointer to the the previous arena and the size of the current one.
432
478
  return divide_round_up<alignment_ak>(sizeof(byte_t*) + sizeof(std::size_t)) * alignment_ak;
@@ -435,8 +481,33 @@ template <std::size_t alignment_ak = 1> class memory_mapping_allocator_gt {
435
481
  std::mutex mutex_;
436
482
  byte_t* last_arena_ = nullptr;
437
483
  std::size_t last_usage_ = head_size();
438
- std::size_t last_capacity_ = min_size();
484
+ std::size_t last_capacity_ = min_capacity();
485
+ std::size_t wasted_space_ = 0;
439
486
 
487
+ public:
488
+ using value_type = byte_t;
489
+ using size_type = std::size_t;
490
+ using pointer = byte_t*;
491
+ using const_pointer = byte_t const*;
492
+
493
+ memory_mapping_allocator_gt() = default;
494
+ memory_mapping_allocator_gt(memory_mapping_allocator_gt&& other) noexcept
495
+ : last_arena_(exchange(other.last_arena_, nullptr)), last_usage_(exchange(other.last_usage_, 0)),
496
+ last_capacity_(exchange(other.last_capacity_, 0)), wasted_space_(exchange(other.wasted_space_, 0)) {}
497
+
498
+ memory_mapping_allocator_gt& operator=(memory_mapping_allocator_gt&& other) noexcept {
499
+ std::swap(last_arena_, other.last_arena_);
500
+ std::swap(last_usage_, other.last_usage_);
501
+ std::swap(last_capacity_, other.last_capacity_);
502
+ std::swap(wasted_space_, other.wasted_space_);
503
+ return *this;
504
+ }
505
+
506
+ ~memory_mapping_allocator_gt() noexcept { reset(); }
507
+
508
+ /**
509
+ * @brief Discards all previously allocated memory buffers.
510
+ */
440
511
  void reset() noexcept {
441
512
  byte_t* last_arena = last_arena_;
442
513
  while (last_arena) {
@@ -444,74 +515,113 @@ template <std::size_t alignment_ak = 1> class memory_mapping_allocator_gt {
444
515
  std::memcpy(&previous_arena, last_arena, sizeof(byte_t*));
445
516
  std::size_t current_size;
446
517
  std::memcpy(&current_size, last_arena + sizeof(byte_t*), sizeof(std::size_t));
518
+ #if defined(USEARCH_DEFINED_WINDOWS)
519
+ ::VirtualFree(last_arena, 0, MEM_RELEASE);
520
+ #else
447
521
  munmap(last_arena, current_size);
522
+ #endif
448
523
  last_arena = previous_arena;
449
524
  }
450
525
 
451
526
  // Clear the references:
452
527
  last_arena_ = nullptr;
453
528
  last_usage_ = head_size();
454
- last_capacity_ = min_size();
455
- }
456
-
457
- public:
458
- using value_type = byte_t;
459
- using size_type = std::size_t;
460
- using pointer = byte_t*;
461
- using const_pointer = byte_t const*;
462
-
463
- memory_mapping_allocator_gt() = default;
464
- memory_mapping_allocator_gt(memory_mapping_allocator_gt&& other) noexcept
465
- : last_arena_(other.last_arena_), last_usage_(other.last_usage_), last_capacity_(other.last_capacity_) {}
466
- memory_mapping_allocator_gt& operator=(memory_mapping_allocator_gt&& other) noexcept {
467
- std::swap(last_arena_, other.last_arena_);
468
- std::swap(last_usage_, other.last_usage_);
469
- std::swap(last_capacity_, other.last_capacity_);
470
- return *this;
529
+ last_capacity_ = min_capacity();
530
+ wasted_space_ = 0;
471
531
  }
472
532
 
533
+ /**
534
+ * @brief Copy constructor.
535
+ * @note This is a no-op copy constructor since the allocator is not copyable.
536
+ */
473
537
  memory_mapping_allocator_gt(memory_mapping_allocator_gt const&) noexcept {}
538
+
539
+ /**
540
+ * @brief Copy assignment operator.
541
+ * @note This is a no-op copy assignment operator since the allocator is not copyable.
542
+ * @return Reference to the allocator after the assignment.
543
+ */
474
544
  memory_mapping_allocator_gt& operator=(memory_mapping_allocator_gt const&) noexcept {
475
545
  reset();
476
546
  return *this;
477
547
  }
478
548
 
479
- ~memory_mapping_allocator_gt() noexcept { reset(); }
480
-
549
+ /**
550
+ * @brief Allocates an @b uninitialized block of memory of the specified size.
551
+ * @param count_bytes The number of bytes to allocate.
552
+ * @return A pointer to the allocated memory block, or `nullptr` if allocation fails.
553
+ */
481
554
  inline byte_t* allocate(std::size_t count_bytes) noexcept {
482
- count_bytes = divide_round_up<alignment_ak>(count_bytes) * alignment_ak;
555
+ std::size_t extended_bytes = divide_round_up<alignment_ak>(count_bytes) * alignment_ak;
556
+ // Check if the requested allocation size is absurd
557
+ if (extended_bytes > min_capacity())
558
+ return nullptr;
483
559
 
484
560
  std::unique_lock<std::mutex> lock(mutex_);
485
- if (!last_arena_ || (last_usage_ + count_bytes > last_capacity_)) {
486
- std::size_t new_capacity = last_capacity_ * 2;
487
- int prot = PROT_WRITE | PROT_READ;
488
- int flags = MAP_PRIVATE | MAP_ANONYMOUS;
489
- byte_t* new_arena = (byte_t*)mmap(NULL, new_capacity, prot, flags, 0, 0);
561
+ if (!last_arena_ || (last_usage_ + extended_bytes > last_capacity_)) {
562
+ std::size_t new_cap = last_capacity_ * capacity_multiplier();
563
+ #if defined(USEARCH_DEFINED_WINDOWS)
564
+ byte_t* new_arena = (byte_t*)(::VirtualAlloc(NULL, new_cap, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
565
+ if (new_arena == nullptr)
566
+ return nullptr;
567
+ #else
568
+ byte_t* new_arena = (byte_t*)mmap(NULL, new_cap, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
569
+ if (!new_arena)
570
+ return nullptr;
571
+ #endif
490
572
  std::memcpy(new_arena, &last_arena_, sizeof(byte_t*));
491
- std::memcpy(new_arena + sizeof(byte_t*), &new_capacity, sizeof(std::size_t));
573
+ std::memcpy(new_arena + sizeof(byte_t*), &new_cap, sizeof(std::size_t));
492
574
 
575
+ wasted_space_ += total_reserved();
493
576
  last_arena_ = new_arena;
494
- last_capacity_ = new_capacity;
577
+ last_capacity_ = new_cap;
495
578
  last_usage_ = head_size();
496
579
  }
497
580
 
498
- return last_arena_ + exchange(last_usage_, last_usage_ + count_bytes);
581
+ wasted_space_ += extended_bytes - count_bytes;
582
+ return last_arena_ + exchange(last_usage_, last_usage_ + extended_bytes);
499
583
  }
500
584
 
501
585
  /**
502
- * @warning The very first memory de-allocation discards all the arenas!
586
+ * @brief Returns the amount of memory used by the allocator across all arenas.
587
+ * @return The amount of space in bytes.
503
588
  */
504
- void deallocate(byte_t*, std::size_t) noexcept { reset(); }
505
- };
589
+ std::size_t total_allocated() const noexcept {
590
+ if (!last_arena_)
591
+ return 0;
592
+ std::size_t total_used = 0;
593
+ std::size_t last_capacity = last_capacity_;
594
+ do {
595
+ total_used += last_capacity;
596
+ last_capacity /= capacity_multiplier();
597
+ } while (last_capacity >= min_capacity());
598
+ return total_used;
599
+ }
506
600
 
507
- using memory_mapping_allocator_t = memory_mapping_allocator_gt<>;
601
+ /**
602
+ * @brief Returns the amount of wasted space due to alignment.
603
+ * @return The amount of wasted space in bytes.
604
+ */
605
+ std::size_t total_wasted() const noexcept { return wasted_space_; }
508
606
 
509
- #else
607
+ /**
608
+ * @brief Returns the amount of remaining memory already reserved but not yet used.
609
+ * @return The amount of reserved memory in bytes.
610
+ */
611
+ std::size_t total_reserved() const noexcept { return last_capacity_ - last_usage_; }
510
612
 
511
- using memory_mapping_allocator_t = aligned_allocator_t;
613
+ /**
614
+ * @warning The very first memory de-allocation discards all the arenas!
615
+ */
616
+ void deallocate(byte_t* = nullptr, std::size_t = 0) noexcept { reset(); }
617
+ };
512
618
 
513
- #endif
619
+ using memory_mapping_allocator_t = memory_mapping_allocator_gt<>;
514
620
 
621
+ /**
622
+ * @brief Utility class used to cast arrays of one scalar type to another,
623
+ * avoiding unnecessary conversions.
624
+ */
515
625
  template <typename from_scalar_at, typename to_scalar_at> struct cast_gt {
516
626
  inline bool operator()(byte_t const* input, std::size_t dimensions, byte_t* output) const {
517
627
  from_scalar_at const* typed_input = reinterpret_cast<from_scalar_at const*>(input);
@@ -12,7 +12,7 @@ npm install usearch
12
12
  For latter:
13
13
 
14
14
  ```sh
15
-
15
+ wasmer install unum/usearch
16
16
  ```
17
17
 
18
18
  ## Quickstart
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "0.22.1",
3
+ "version": "0.22.3",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ashot Vardanian",
6
6
  "license": "Apache 2.0",
package/pyproject.toml CHANGED
@@ -16,7 +16,7 @@ skip = ["*musllinux*", "*i686*", "pp*", "cp36-*"]
16
16
 
17
17
  [tool.cibuildwheel.linux]
18
18
  before-build = [
19
- "rm -rf {project}/CMakeCache.txt {project}/build {project}/build_debug {project}/CMakeFiles.txt {project}/_deps",
19
+ "rm -rf {project}/CMakeCache.txt {project}/build {project}/build_debug {project}/CMakeFiles.txt {project}/_deps {project}/.pytest_cache",
20
20
  "mkdir -p build/usearch",
21
21
  "git submodule update --init --recursive"
22
22
  ]
@@ -31,7 +31,7 @@ repair-wheel-command = "auditwheel repair --lib-sdir . -w {dest_dir} {wheel}"
31
31
 
32
32
  [tool.cibuildwheel.macos]
33
33
  before-build = [
34
- "rm -rf {project}/CMakeCache.txt {project}/build {project}/build_debug {project}/CMakeFiles.txt {project}/_deps",
34
+ "rm -rf {project}/CMakeCache.txt {project}/build {project}/build_debug {project}/CMakeFiles.txt {project}/_deps {project}/.pytest_cache",
35
35
  "mkdir -p build/usearch",
36
36
  "git submodule update --init --recursive"
37
37
  ]
@@ -41,7 +41,7 @@ repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest
41
41
 
42
42
  [tool.cibuildwheel.windows]
43
43
  before-build = [
44
- "rd /s /q {project}\\CMakeCache.txt {project}\\build {project}\\build_debug {project}\\CMakeFiles.txt {project}\\_deps || echo Done",
44
+ "rd /s /q {project}\\CMakeCache.txt {project}\\build {project}\\build_debug {project}\\CMakeFiles.txt {project}\\_deps {project}\\.pytest_cache || echo Done",
45
45
  "md build\\usearch",
46
46
  "git submodule update --init --recursive"
47
47
  ]
package/python/lib.cpp CHANGED
@@ -687,6 +687,8 @@ PYBIND11_MODULE(compiled, m) {
687
687
  [](dense_index_py_t& index, label_t label, bool compact, std::size_t threads) -> bool {
688
688
  dense_labeling_result_t result = index.remove(label);
689
689
  result.error.raise();
690
+ if (!compact)
691
+ return result.completed;
690
692
 
691
693
  if (!threads)
692
694
  threads = std::thread::hardware_concurrency();
@@ -704,6 +706,8 @@ PYBIND11_MODULE(compiled, m) {
704
706
  std::size_t threads) -> std::size_t {
705
707
  dense_labeling_result_t result = index.remove(labels.begin(), labels.end());
706
708
  result.error.raise();
709
+ if (!compact)
710
+ return result.completed;
707
711
 
708
712
  if (!threads)
709
713
  threads = std::thread::hardware_concurrency();
package/wasmer.toml CHANGED
@@ -1,7 +1,11 @@
1
1
  [package]
2
2
  name="unum/usearch"
3
- version="0.19.3"
3
+ version="0.22.3"
4
4
  description="Smaller & Faster Single-File Vector Search Engine from Unum"
5
+ license="Apache-2.0"
6
+ readme="README.md"
7
+ repository="https://github.com/unum-cloud/usearch"
8
+ homepage="https://unum-cloud.github.io/usearch/javascript"
5
9
 
6
10
  [[module]]
7
11
  name="index"