Skip to content

Performance

Optimization Tips

1. Use Column Indices Instead of Names

Map lookups are slower than direct index access:

// Slow: map lookup every call
for (int row = 0; row < bank.getRows(); row++) {
    int pid = bank.getInt("pid", row);  // map lookup
}

// Fast: cache the index
int pidCol = bank.getSchema().getEntryOrder("pid");
for (int row = 0; row < bank.getRows(); row++) {
    int pid = bank.getInt(pidCol, row);  // direct access
}

2. Reuse Bank Objects

Banks are designed to be reused across events. Don't create new bank objects inside the event loop:

// Good: create once, reuse
hipo::bank particles(dict.getSchema("REC::Particle"));
while (reader.next()) {
    event.read(particles);  // overwrites in-place
}

// Bad: creates a new bank every event
while (reader.next()) {
    hipo::bank particles(dict.getSchema("REC::Particle"));  // allocation!
    event.read(particles);
}

3. Use Banklist for Multiple Banks

The next(banklist) variant reads all banks in one call, avoiding redundant event parsing:

auto list = reader.getBanks({"REC::Particle", "REC::Event"});
while (reader.next(list)) {
    // more efficient than separate event.read() calls
}

4. Cache Parser Objects

When filtering rows across many events:

// Good: parse expression once
hipo::Parser filter("pid==11&&charge<0");
while (reader.next(list)) {
    list[0].getMutableRowList().filter(filter);
}

// Bad: re-parses expression every event
while (reader.next(list)) {
    list[0].getMutableRowList().filter("pid==11&&charge<0");
}

5. Use Chain for Multi-File Processing

The chain class handles multi-file processing with optimal thread utilization:

hipo::chain ch(0);  // auto-detect thread count
ch.add_pattern("data/*.hipo");
ch.process([](auto& event, int file_idx, long event_idx) {
    // automatically parallelized at record level
});

Performance Characteristics

Read Performance

Operation Typical Speed
LZ4 decompression 1-2 GB/s
Sequential event iteration ~1M events/sec
Bank column access (by index) ~ns per access
Bank column access (by name) ~100ns per access

Record Sizing

Default record limits:

  • 100,000 events per record
  • 8 MB uncompressed data per record

These are tuned for typical CLAS12 event sizes. The record size affects:

  • Larger records: Better compression ratio, larger memory footprint
  • Smaller records: Lower latency for random access, more I/O overhead

Compression

LZ4 compression ratio for typical physics data:

  • Raw events: ~5-10x compression
  • Decompression speed: ~1-2 GB/s (CPU-limited, not I/O-limited)

Limitations

No Random Column Access

HIPO's columnar layout is within each bank, not across the file. You cannot efficiently read "column X from all events" without decompressing all records. For this pattern, use record::getColumn().

No Parallel Writing

The writer class is single-threaded. For parallel workflows, use separate writers for separate output files.

Event Size Limit

The default event buffer is 128 KB. Events exceeding this size will cause buffer overflows.

No In-Place Modification

HIPO files cannot be modified in-place. To update data, read from one file and write to another.

Benchmarking

Use the built-in benchmark class to measure performance:

hipo::benchmark readBench;
hipo::benchmark processBench;

while (reader.next()) {
    readBench.resume();
    reader.read(event);
    event.read(particles);
    readBench.pause();

    processBench.resume();
    // analysis code...
    processBench.pause();
}

printf("Read time: %.3f sec\n", readBench.getTimeSec());
printf("Process time: %.3f sec\n", processBench.getTimeSec());

Record-level benchmarks are also available:

auto& readBM  = record.getReadBenchmark();
auto& unzipBM = record.getUnzipBenchmark();
printf("Disk read: %.3f sec, LZ4 decompress: %.3f sec\n",
       readBM.getTimeSec(), unzipBM.getTimeSec());