Skip to content

Batch Read Loop Unrolling Optimization for Dictionary Reads#135

Open
siddharthteotia wants to merge 2 commits into
masterfrom
dict_perf
Open

Batch Read Loop Unrolling Optimization for Dictionary Reads#135
siddharthteotia wants to merge 2 commits into
masterfrom
dict_perf

Conversation

@siddharthteotia

@siddharthteotia siddharthteotia commented Feb 16, 2026

Copy link
Copy Markdown
Collaborator

Problem and Solution Overview

JIRA: OA-837

Optimized dictionary batch read operations through 8x loop unrolling for all numeric types (int, long, float, double).

Before (Scalar Loop) - One value per iteration, high branch overhead, CPU pipeline underutilized.

default void readIntValues(int[] dictIds, int length, int[] outValues) {
  for (int i = 0; i < length; i++) {
    outValues[i] = getIntValue(dictIds[i]);
  }
}

After (8x Loop Unrolling)

default void readIntValues(int[] dictIds, int length, int[] outValues) {
  int i = 0;
  int limit = length - 7;

  // Process 8 values at a time
  for (; i < limit; i += 8) {
    outValues[i] = getIntValue(dictIds[i]);
    outValues[i + 1] = getIntValue(dictIds[i + 1]);
    outValues[i + 2] = getIntValue(dictIds[i + 2]);
    .......
    .......
  }

  // Handle remaining 0-7 elements
  for (; i < length; i++) {
    outValues[i] = getIntValue(dictIds[i]);
  }
}

Benefits

  • 8x fewer branch checks
  • CPU executes multiple memory loads in parallel (out-of-order execution)
  • Better instruction-level parallelism
  • Reduced loop counter overhead

Solution Details

All below methods in the base Dictionary implementation are optimized

Primitive types:

  • readIntValues(int[] dictIds, int length, int[] outValues)
  • readLongValues(int[] dictIds, int length, long[] outValues)
  • readFloatValues(int[] dictIds, int length, float[] outValues)
  • readDoubleValues(int[] dictIds, int length, double[] outValues)

Boxed types:

  • readIntValues(int[] dictIds, int length, Integer[] outValues)
  • readLongValues(int[] dictIds, int length, Long[] outValues)
  • readFloatValues(int[] dictIds, int length, Float[] outValues)
  • readDoubleValues(int[] dictIds, int length, Double[] outValues)

All implementations using default methods automatically benefit:

  • Off-Heap - IntDictionary, LongDictionary, FloatDictionary, DoubleDictionary
  • On-Heap - OnHeapIntDictionary, OnHeapLongDictionary, OnHeapFloatDictionary, OnHeapDoubleDictionary
  • Mutable (Realtime) - All numeric mutable dictionaries

Testing Done

  • New Unit Tests in DictionaryBatchReadTest.java
    -- 18 test cases covering boundary conditions (empty, <8, =8, =16, >8 elements, random access)

  • New Performance Tests in JMBenchmarkDictionaryBatchRead.java

  • Backward compatible (no API changes)

Performance Results

| Batch Size | Access Pattern | Dictionary Type | Speedup   | Improvement  | Notes |
|------------|----------------|-----------------|-----------|--------------|-------|
| 100        | Sequential     | On-Heap         | 3x | 150.9% | Best case scenario |
| 100        | Sequential     | Off-Heap        | 1.05x     | +4.8%        | |
| 100        | Random         | On-Heap         | 1.11x     | +11.2% ✅    | |
| 100        | Random         | Off-Heap        | 0.95x     | -4.8%        | Memory I/O bottleneck |
| 1,000      | Sequential     | On-Heap         | ~1.0x     | 0-1%         | |
| 1,000      | Sequential     | Off-Heap        | 1.02x     | +1.7%        | |
| 1,000      | Random         | On-Heap         | 1.06x     | +6.0% ✅     | |
| 1,000      | Random         | Off-Heap        | ~1.0x     | 0-1%         | |
| 10,000     | Sequential     | On-Heap         | ~1.2x     | ~small%          | JIT optimization dominates |
| 10,000     | Sequential     | Off-Heap        | ~1.2x     | ~small%          | JIT optimization dominates |
| 10,000     | Random         | On-Heap         | ~1.2x     | ~small%          | JIT optimization dominates |

Why On-Heap Benefits More:

  • Direct array access (int[] _dictIdToVal)
  • Better CPU cache locality
  • No memory-mapped I/O overhead

Why Off-Heap Shows Less Improvement:

  • Memory-mapped I/O becomes bottleneck
  • JIT compiler potentially already optimizes after sufficient warmup cycles have been done and C2 kicks in
  • Cache misses dominate for random access

Future Optimizations

On-Heap Dictionary Specialization

  • Override batch read methods in on-heap dictionaries with:
  • Direct array access (eliminate virtual method call overhead)
  • Sequential access detection → System.arraycopy()

Software Prefetching

  • Add software prefetch hints to hide memory latency for off-heap dictionaries.

SIMD Kernel

  • Via Vector API (requires JDK16+) or JNI Pushdown in C++ or Rust (Work in Progress)

References

Consolidate performance results into a single comprehensive table showing
all batch sizes, access patterns, and dictionary types for easier comparison.
Add detailed technical explanation of JIT compiler behavior and why manual
loop unrolling benefits small batches but shows no improvement at large batch
sizes where JIT optimization dominates. Format all tables with proper alignment.
@siddharthteotia siddharthteotia changed the title Batch Read Loop Unrolling Optimization for Dictionary Reads [WIP] Batch Read Loop Unrolling Optimization for Dictionary Reads Feb 16, 2026
@siddharthteotia siddharthteotia changed the title [WIP] Batch Read Loop Unrolling Optimization for Dictionary Reads Batch Read Loop Unrolling Optimization for Dictionary Reads Mar 26, 2026
@siddharthteotia

Copy link
Copy Markdown
Collaborator Author

Some weird licensing issue is failing tests. Will look locally via claude and fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant