Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ cmake_minimum_required(VERSION 3.20)

option(LASPP_DEBUG_ASSERTS "Enable debugging asserts" ON)
option(LASPP_BUILD_TESTS "Build tests" ON)
option(LASPP_BUILD_BENCHMARK "Build benchmark" ON)
# Default benchmarks to OFF if this is a subproject (to avoid downloading
# lazperf/googletest)
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
set(LASPP_BUILD_BENCHMARK_DEFAULT ON)
else()
set(LASPP_BUILD_BENCHMARK_DEFAULT OFF)
endif()
option(LASPP_BUILD_BENCHMARK "Build benchmark" ${LASPP_BUILD_BENCHMARK_DEFAULT})
option(LASPP_BUILD_APPS "Build apps" ON)
option(LASPP_AGGRESSIVE_OPTIMIZATIONS
"Enable aggressive performance optimizations (-march=native, etc.)" ON)
Expand Down
75 changes: 64 additions & 11 deletions apps/benchmark.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <sstream>
Expand Down Expand Up @@ -192,6 +193,32 @@ static std::string json_escape(const std::string& s) {
return out;
}

// ── Raw file read (roofline): read entire file into memory without decompression ───

static double raw_file_read_once(const std::filesystem::path& path) {
auto t0 = Clock::now();
{
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
throw std::runtime_error("Cannot open file: " + path.string());
}
std::streamsize size = file.tellg();
// Validate tellg() result before using it (returns -1 on failure)
if (size < 0 || !file.good()) {
throw std::runtime_error("Failed to get file size: " + path.string());
}
file.seekg(0, std::ios::beg);
if (!file.good()) {
throw std::runtime_error("Failed to seek to beginning: " + path.string());
}
std::vector<char> buffer(static_cast<std::size_t>(size));
if (!file.read(buffer.data(), size)) {
throw std::runtime_error("Failed to read file: " + path.string());
}
}
return elapsed_since(t0);
}

// ── LAZperf: single-threaded read ────────────────────────────────────────────

#ifdef LASPP_HAS_LAZPERF
Expand Down Expand Up @@ -256,14 +283,26 @@ static double laspp_read_once(const std::filesystem::path& path, int n_threads)
ThreadControl ctrl(n_threads);
auto t0 = Clock::now();
{
std::ifstream ifs(path, std::ios::binary);
LASReader reader(ifs);
// Use file path constructor - automatically uses memory mapping for optimal performance
LASReader reader(path);
std::vector<PointType> points(reader.num_points());
reader.read_chunks<PointType>(points, {0, reader.num_chunks()});
}
return elapsed_since(t0);
}

// Check memory mapping status once per thread configuration (called before warmup/iterations)
template <typename PointType>
static void check_memory_mapping_status(const std::filesystem::path& path, int n_threads) {
ThreadControl ctrl(n_threads);
LASReader reader(path);
if (!reader.is_using_memory_mapping()) {
std::cerr << "WARNING: LASReader is NOT using memory mapping (fell back to streams)\n";
} else {
std::cerr << "INFO: LASReader is using memory-mapped file I/O\n";
}
}

// ── Point-format helpers ──────────────────────────────────────────────────────

template <typename T>
Expand Down Expand Up @@ -323,8 +362,8 @@ static void run_benchmark(const std::filesystem::path& path, bool do_read, bool
// Pre-read points into memory for the write benchmark (not timed).
std::vector<PointType> cached_points;
if (do_write) {
std::ifstream ifs(path, std::ios::binary);
LASReader rdr(ifs);
// Use file path constructor - automatically uses memory mapping for optimal performance
LASReader rdr(path);
cached_points.resize(rdr.num_points());
rdr.read_chunks<PointType>(cached_points, {0, rdr.num_chunks()});
}
Expand All @@ -334,9 +373,18 @@ static void run_benchmark(const std::filesystem::path& path, bool do_read, bool
for (int n_threads : thread_counts) {
// ── laspp read ──────────────────────────────────────────────────────────
if (do_read) {
// Check memory mapping status once per thread configuration (before timing)
// Use static map to track which thread counts we've already checked
static std::map<int, bool> checked_threads;
if (checked_threads.find(n_threads) == checked_threads.end()) {
check_memory_mapping_status<PointType>(path, n_threads);
checked_threads[n_threads] = true;
}
// Warmup iterations (not timed)
for (int w = 0; w < warmup; ++w) {
laspp_read_once<PointType>(path, n_threads);
}
// Timed iterations
for (int it = 0; it < iterations; ++it) {
double t = laspp_read_once<PointType>(path, n_threads);
results.push_back({"laspp", "read", n_threads, it, t});
Expand Down Expand Up @@ -383,6 +431,17 @@ static void run_benchmark(const std::filesystem::path& path, bool do_read, bool
}
}
#endif

// ── Raw file read (roofline) ─────────────────────────────────────────────
if (do_read) {
for (int w = 0; w < warmup; ++w) {
raw_file_read_once(path);
}
for (int it = 0; it < iterations; ++it) {
double t = raw_file_read_once(path);
results.push_back({"roofline", "read", 0, it, t});
}
}
}

// ── main ──────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -479,17 +538,11 @@ int main(int argc, char* argv[]) {

// ── Probe file header ─────────────────────────────────────────────────────

std::ifstream probe(path, std::ios::binary);
if (!probe.is_open()) {
std::cerr << "Error: cannot open: " << file_str << "\n";
return 1;
}
LASReader probe_reader(probe);
LASReader probe_reader(path);
uint8_t point_format_byte = probe_reader.header().point_format();
uint8_t base_format = static_cast<uint8_t>(point_format_byte & 0x7Fu);
bool is_compressed = (point_format_byte & 0x80u) != 0u;
std::size_t num_points = probe_reader.num_points();
probe.close();

std::uintmax_t file_size_bytes = std::filesystem::file_size(path);
int hw_threads = static_cast<int>(std::thread::hardware_concurrency());
Expand Down
29 changes: 19 additions & 10 deletions apps/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,10 +624,10 @@ def plot_results(

# Plot thread-scaling tools as lines
for tool in scaling_tools:
# Get all points including default (threads=0)
# Get all points (exclude threads=0/default)
tool_pts = sorted(
[(s.threads, s.mb_per_s)
for s in op_results if s.tool == tool and s.threads >= 0],
for s in op_results if s.tool == tool and s.threads > 0],
key=lambda x: x[0],
)
if tool_pts:
Expand All @@ -642,17 +642,30 @@ def plot_results(
# Single-threaded tools as horizontal dashed lines
single_threaded_tools = ["laszip", "lazperf", "pdal"]

# Roofline (raw file read) as a special horizontal line
roofline_results = [s for s in op_results if s.tool == "roofline"]
if roofline_results:
roofline_result = roofline_results[0]
if "roofline" not in tool_colors:
tool_colors["roofline"] = "#888888" # Gray color for roofline
ax.axhline(roofline_result.mb_per_s, linestyle=":", color=tool_colors["roofline"],
label="Roofline (raw file read)", linewidth=2.0, alpha=0.7)

for tool in single_threaded_tools:
tool_results = [s for s in op_results if s.tool == tool]
if not tool_results:
continue
# Get the single-threaded result
single_thread_result = next((s for s in tool_results if s.threads <= 1), tool_results[0])
# Get the single-threaded result (threads <= 1 or threads == 0 for default)
single_thread_result = next(
(s for s in tool_results if s.threads <= 1 or s.threads == 0),
tool_results[0]
)
if tool not in tool_colors:
# Start color assignment after scaling tools
tool_colors[tool] = colors[len(tool_colors) % len(colors)]
col = tool_colors[tool]
ax.axhline(single_thread_result.mb_per_s, linestyle="--", color=col,
label=tool, linewidth=1.5)
label=tool, linewidth=2.0, alpha=0.8)

ax.set_xlabel("Thread count", fontsize=11)
ax.set_ylabel("Throughput (MB/s – compressed file size)", fontsize=11)
Expand Down Expand Up @@ -760,11 +773,7 @@ def main() -> int:
else:
print(f"Using C++ benchmark binary: {binary}")
include_laszip = "laszip" in tools
if "laspp" in tools:
# Add "default" (0 = unset LASPP_NUM_THREADS) to thread counts
include_laspp_threads = [0] + thread_counts
else:
include_laspp_threads = [1]
include_laspp_threads = thread_counts if "laspp" in tools else [1]
try:
meta, cpp_results = run_cpp_benchmark(
binary=binary,
Expand Down
8 changes: 3 additions & 5 deletions src/las_header.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class Vector3D {
double operator[](size_t i) const { return m_data[i]; }

explicit Vector3D(std::istream& in_stream) {
LASPP_CHECK_READ(in_stream.read(reinterpret_cast<char*>(m_data.data()), sizeof(m_data)));
LASPP_CHECK_READ(in_stream, m_data.data(), sizeof(m_data));
}

Vector3D(double x, double y, double z) : m_data{{x, y, z}} {}
Expand Down Expand Up @@ -279,10 +279,8 @@ class LASHeader {

public:
explicit LASHeader(std::istream& in_stream) {
in_stream.seekg(0);
apply_all_in_order([&](auto& val) {
LASPP_CHECK_READ(in_stream.read(reinterpret_cast<char*>(&val), sizeof(val)));
});
LASPP_CHECK_SEEK(in_stream, 0, std::ios::beg);
apply_all_in_order([&](auto& val) { LASPP_CHECK_READ(in_stream, &val, sizeof(val)); });

// Validate header_size matches version
if (m_version_major == 1 && m_version_minor == 4) {
Expand Down
Loading