LAZ vs Uncompressed LAS for Iterative Processing
TL;DR: During tight edit-run development loops where you re-read the same tile dozens of times, keep it as uncompressed .las to skip the LASzip decompression tax on every run; switch back to .laz for archival, transfer, and anything you touch infrequently — the compression saves 5–8x on disk but costs 2–4x on each read.
# Context and Motivation
This guide is part of Memory Management, which treats data volume as the dominant cost in Python LiDAR work. Here the question is narrower and it is about time, not RAM: when you iterate on a pipeline against the same file over and over, does compression help or hurt?
Compression is almost always the right default for stored point clouds — a LAZ tile is a fraction of the bytes of its LAS twin, and it moves across a network in a fraction of the time. But iterative development inverts the usual economics. In a debugging loop you might execute a pipeline against one tile forty times in an afternoon, tweaking a filters.smrf slope or a range predicate between runs. Every one of those runs pays the full LASzip decompression cost before the first filter even sees a point. That per-run tax is invisible in a benchmark that reads a file once, but it accumulates into minutes of dead waiting across a working session. Understanding where the cost lands — disk once versus CPU every time — is what lets you pick the right format for the phase of work you are in, rather than reflexively compressing everything.
# Prerequisites and Assumptions
| Requirement | Detail |
|---|---|
| PDAL | 2.5+ with LASzip support compiled in |
Python pdal bindings |
3.x (pip install pdal) |
laspy (optional cross-check) |
2.4+ with laspy[lazrs] backend |
| Sample tile | 5–50 M points, any real acquisition (USGS 3DEP works well) |
| Disk | Local SSD/NVMe — network storage skews read timings toward I/O |
This comparison assumes you already know how PDAL streams stages; if not, the PDAL Pipeline Architecture & Execution overview covers the reader-filter-writer model that every timing below sits inside. The measurements here reflect a single tile read repeatedly on one machine; parallelising the loop is a separate concern covered in Optimizing PDAL for Multi-Core Processing.
# Step-by-Step Comparison
# Step 1 — Stage one identical copy in each format
Start from your source tile and write two working copies with the same point content — one compressed, one not. The only difference is the compression flag on writers.las.
{
"pipeline": [
"source_tile.laz",
{
"type": "writers.las",
"filename": "work_copy.las",
"compression": "none",
"forward": "all"
}
]
}Swap "compression": "none" for "compression": "laszip" and the .las extension for .laz to produce the compressed twin. forward: "all" guarantees both copies carry identical headers, VLRs, and extra dimensions, so the benchmark compares like with like. For the mechanics of that conversion in isolation, see Converting LAS to LAZ with PDAL.
# Step 2 — Time a repeated read of each file
A read-only pipeline — a bare reader, nothing else — isolates decompression cost from filter cost. Execute it several times per format and record the wall-clock time of each run.
import json
import time
import pdal
def time_reads(path: str, iterations: int = 5) -> list[float]:
"""Return per-iteration wall-clock read times (seconds) for one file."""
durations = []
for _ in range(iterations):
t0 = time.perf_counter()
p = pdal.Pipeline(json.dumps({"pipeline": [path]}))
p.execute()
durations.append(time.perf_counter() - t0)
return durations# Step 3 — Record file size and CPU seconds
Wall-clock time conflates disk I/O with CPU decode work. Sampling time.process_time() alongside it separates the two — LAZ inflates CPU time because LASzip runs on the processor, whereas LAS spends its budget mostly waiting on disk.
import os
def measure(path: str) -> dict:
size_mb = os.stat(path).st_size / 1024**2
t_wall = time.perf_counter()
t_cpu = time.process_time()
pdal.Pipeline(json.dumps({"pipeline": [path]})).execute()
return {
"path": path,
"size_mb": round(size_mb, 1),
"wall_s": round(time.perf_counter() - t_wall, 3),
"cpu_s": round(time.process_time() - t_cpu, 3),
}# Step 4 — Decide by the phase of work
The numbers point to a rule of thumb rather than a universal winner. If you are re-running a pipeline against the same tile many times in one sitting, the uncompressed copy pays for itself within a handful of iterations. If the file mostly sits in storage or crosses a network, keep it compressed and eat the occasional decode.
# Complete Working Example
Save the following as laz_las_bench.py. It stages both formats from a single source, runs a repeated-read benchmark, and prints a comparison table with size, median read time, and CPU cost.
#!/usr/bin/env python3
"""
laz_las_bench.py
Benchmark compressed LAZ against uncompressed LAS for repeated reads.
Usage:
python laz_las_bench.py source_tile.laz --iterations 7
Requirements:
pip install pdal
PDAL 2.5+ with LASzip
"""
import argparse
import json
import os
import statistics
import time
import pdal
def stage_copy(source: str, out_path: str, compression: str) -> None:
"""Write an identical-content working copy in the requested compression mode."""
pipeline = {
"pipeline": [
source,
{
"type": "writers.las",
"filename": out_path,
"compression": compression, # "none" or "laszip"
"forward": "all",
},
]
}
pdal.Pipeline(json.dumps(pipeline)).execute()
def bench_reads(path: str, iterations: int) -> dict:
"""Read a file repeatedly; return size and timing statistics."""
wall, cpu = [], []
for _ in range(iterations):
t_wall, t_cpu = time.perf_counter(), time.process_time()
p = pdal.Pipeline(json.dumps({"pipeline": [path]}))
count = p.execute()
wall.append(time.perf_counter() - t_wall)
cpu.append(time.process_time() - t_cpu)
return {
"path": path,
"points": count,
"size_mb": round(os.stat(path).st_size / 1024**2, 1),
"median_wall_s": round(statistics.median(wall), 3),
"median_cpu_s": round(statistics.median(cpu), 3),
}
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("source")
ap.add_argument("--iterations", type=int, default=7)
args = ap.parse_args()
print("Staging working copies (identical point content)...")
stage_copy(args.source, "work_copy.las", "none")
stage_copy(args.source, "work_copy.laz", "laszip")
print(f"Benchmarking {args.iterations} reads per format...\n")
las = bench_reads("work_copy.las", args.iterations)
laz = bench_reads("work_copy.laz", args.iterations)
hdr = f"{'format':<8}{'size (MB)':>12}{'read wall (s)':>16}{'read cpu (s)':>15}"
print(hdr)
print("-" * len(hdr))
for tag, r in (("LAS", las), ("LAZ", laz)):
print(f"{tag:<8}{r['size_mb']:>12}{r['median_wall_s']:>16}{r['median_cpu_s']:>15}")
size_ratio = laz["size_mb"] and round(las["size_mb"] / laz["size_mb"], 1)
read_ratio = las["median_wall_s"] and round(laz["median_wall_s"] / las["median_wall_s"], 1)
print(
f"\nLAZ is {size_ratio}x smaller on disk but {read_ratio}x slower to read.\n"
f"Break-even: keep LAS if you will re-read more than a few times this session."
)
if __name__ == "__main__":
main()Representative output on an 18 M-point tile (point format 6, NAD83 UTM Zone 12N, EPSG:6341) reads roughly:
format size (MB) read wall (s) read cpu (s)
------------------------------------------------------
LAS 412.0 0.91 0.34
LAZ 61.5 2.73 2.41LAZ is about 6.7x smaller on disk, but each read costs roughly 3x the wall time and 7x the CPU. Over forty iterations in a debugging session, that difference is minutes of the uncompressed copy sitting idle while the compressed one grinds.
# Key Parameter Table
| Parameter | Stage | Values | Effect |
|---|---|---|---|
compression |
writers.las |
"none", "laszip" |
"laszip" produces .laz; controls on-disk size, not in-memory footprint |
forward |
writers.las |
"all", "header", dim list |
"all" keeps both copies byte-identical in content for a fair test |
minor_version |
writers.las |
2–4 |
Point format 6+ requires 4; keep identical across both copies |
iterations |
benchmark | int | More runs smooth out OS page-cache warm-up on the first read |
A subtle confound: the OS page cache makes the second read of any file faster than the first, because the bytes are already in RAM. This helps LAS and LAZ equally on wall time, but it never removes LAZ’s CPU decode cost — the processor still reconstructs every point from the cached compressed bytes. That is why the CPU column is the honest signal for iterative work.
# Verification
Confirm the two working copies genuinely hold the same points before trusting any timing. A mismatch means forward dropped something and the comparison is invalid.
import json
import pdal
def assert_identical(las_path: str, laz_path: str) -> None:
def load(path):
p = pdal.Pipeline(json.dumps({"pipeline": [path]}))
p.execute()
return p.arrays[0]
a, b = load(las_path), load(laz_path)
assert a.shape[0] == b.shape[0], "Point counts differ — forward:all was not honoured"
for dim in ("X", "Y", "Z", "Intensity", "Classification"):
assert (a[dim] == b[dim]).all(), f"Dimension {dim} differs between copies"
print(f"OK: {a.shape[0]:,} points identical across both formats.")Because LASzip is lossless, this assertion passes exactly — every stored integer round-trips. If it ever fails, suspect a differing forward setting or a point format downgrade, not compression itself.
# Gotchas and Edge Cases
1. Timing the first read of a cold file measures your disk, not the format. The very first read after writing a file may hit disk while later reads hit the page cache. Discard the first iteration or read each file once as a warm-up before recording, otherwise LAS looks artificially slow on spinning disks and LAZ looks artificially competitive.
2. LAZ and LAS use identical RAM once loaded.
It is tempting to reach for LAZ to fit a bigger tile in memory — but compression is a disk property only. pipeline.execute() expands both to the same NumPy array. If RAM is the constraint, tiling and dtype downcasting from the Memory Management guide are the levers, not the file format.
3. Multi-threaded LASzip changes the equation on many-core machines.
The lazrs_parallel backend decompresses across threads and narrows LAZ’s read penalty considerably. If your development box has 16+ cores mostly idle during a read, benchmark with the parallel backend before assuming uncompressed LAS wins — the CPU cost is still there, but wall time may be close enough that the disk savings dominate.
4. Leaving uncompressed scratch files behind fills disks fast. An uncompressed working copy of a large survey can be many gigabytes. Treat these as disposable scratch: write them to a temp directory, and re-compress or delete them at the end of the session rather than committing them to shared storage where they multiply.
# Frequently Asked Questions
How much slower is reading LAZ than uncompressed LAS in PDAL?
On a typical 8-core workstation, decompressing LAZ adds roughly 2–4x to the read time of an equivalent LAS file, because LASzip must reconstruct every point record before PDAL can hand the buffer to the next stage. The exact ratio depends on point format width and disk speed, but the CPU cost is real and recurs on every single run.
Does LAZ use less RAM than LAS once loaded?
No. Compression only affects the on-disk representation. Once PDAL materialises the point buffer, LAZ and LAS occupy identical memory because both expand to the same NumPy structured array. The savings are purely storage and transfer bandwidth, not runtime footprint — a distinction the Memory Management guide leans on heavily.
Is LAZ compression lossless?
Yes. LASzip is bit-exact: coordinates, intensity, classification, GPS time, and extra dimensions round-trip identically. Converting LAS to LAZ and back yields the same integer point records, so there is no accuracy reason to avoid it — only a speed reason during repeated reads.
Should I keep intermediate pipeline outputs as LAZ or LAS?
For scratch intermediates that you re-read many times in a single debugging session, uncompressed LAS avoids paying the decompression tax on each iteration. For intermediates that survive between sessions or move across machines, LAZ is worth the compression cost because you read them far less often than you store them. When several tiles are processed at once, Optimizing PDAL for Multi-Core Processing changes the arithmetic by spreading decode across cores.
# Related
- Memory Management — parent guide on RAM discipline, tiling, and why compression does not change in-memory footprint
- PDAL Pipeline Architecture & Execution — the reader-filter-writer streaming model these timings sit inside
- Optimizing PDAL for Multi-Core Processing — parallel decode strategies that narrow LAZ’s read penalty
- Converting LAS to LAZ with PDAL — the lossless conversion that stages both working copies
- LAS/LAZ File Structure — how the binary layout is shared between compressed and uncompressed files