diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index c6e227558e..36a4593aa2 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -60,3 +60,5 @@ function(kvikio_add_benchmark) endfunction() kvikio_add_benchmark(NAME THREADPOOL_BENCHMARK SOURCES "threadpool/threadpool_benchmark.cpp") + +kvikio_add_benchmark(NAME POSIX_BENCHMARK SOURCES "local/posix_benchmark.cpp" "utils.cpp") diff --git a/cpp/benchmarks/local/posix_benchmark.cpp b/cpp/benchmarks/local/posix_benchmark.cpp new file mode 100644 index 0000000000..b9ecdeb6f5 --- /dev/null +++ b/cpp/benchmarks/local/posix_benchmark.cpp @@ -0,0 +1,181 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "posix_benchmark.hpp" +#include "../utils.hpp" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace kvikio::benchmark { +void PosixConfig::parse_args(int argc, char** argv) +{ + Config::parse_args(argc, argv); + static option long_options[] = { + {"overwrite", no_argument, nullptr, 'w'}, {0, 0, 0, 0} + // Sentinel to mark the end of the array. Needed by getopt_long() + }; + + int opt{0}; + int option_index{-1}; + + // "f:"" means "-f" takes an argument + // "c" means "-c" does not take an argument + while ((opt = getopt_long(argc, argv, ":wp", long_options, &option_index)) != -1) { + switch (opt) { + case 'w': { + overwrite_file = true; + break; + } + case 'p': { + per_file_pool = true; + break; + } + case ':': { + // The parsed option has missing argument + std::stringstream ss; + ss << "Missing argument for option " << argv[optind - 1] << " (-" + << static_cast(optopt) << ")"; + throw std::runtime_error(ss.str()); + break; + } + default: { + // Unknown option is deferred to subsequent parsing, if any + break; + } + } + } + + // Reset getopt state for second pass in the future + optind = 1; +} + +void PosixConfig::print_usage(std::string const& program_name) +{ + Config::print_usage(program_name); + std::cout << " -w, --overwrite Overwrite existing file\n"; +} + +PosixBenchmark::PosixBenchmark(PosixConfig config) : Benchmark(std::move(config)) +{ + for (auto const& filepath : _config.filepaths) { + // Initialize buffer + void* buf{}; + + if (_config.align_buffer) { + auto const page_size = get_page_size(); + auto const aligned_size = kvikio::detail::align_up(_config.num_bytes, page_size); + buf = std::aligned_alloc(page_size, aligned_size); + } else { + buf = std::malloc(_config.num_bytes); + } + + std::memset(buf, 0, _config.num_bytes); + + _bufs.push_back(buf); + + // Initialize file + // Create the file if the overwrite flag is on, or if the file does not exist. + if (_config.overwrite_file || access(filepath.c_str(), F_OK) != 0) { + kvikio::FileHandle file_handle(filepath, "w", kvikio::FileHandle::m644); + auto fut = file_handle.pwrite(buf, _config.num_bytes); + fut.get(); + } + + // Initialize thread pool + if (_config.per_file_pool) { + auto thread_pool = std::make_unique(_config.num_threads); + _thread_pools.push_back(std::move(thread_pool)); + } + } +} + +PosixBenchmark::~PosixBenchmark() +{ + for (auto&& buf : _bufs) { + std::free(buf); + } +} + +void PosixBenchmark::initialize_impl() +{ + _file_handles.clear(); + + for (auto const& filepath : _config.filepaths) { + auto p = std::make_unique(filepath, "r"); + + if (_config.o_direct) { + auto file_status_flags = fcntl(p->fd(), F_GETFL); + SYSCALL_CHECK(file_status_flags); + SYSCALL_CHECK(fcntl(p->fd(), F_SETFL, file_status_flags | O_DIRECT)); + } + + _file_handles.push_back(std::move(p)); + } +} + +void PosixBenchmark::cleanup_impl() +{ + for (auto&& file_handle : _file_handles) { + file_handle->close(); + } +} + +void PosixBenchmark::run_target_impl() +{ + std::vector> futs; + + for (std::size_t i = 0; i < _file_handles.size(); ++i) { + auto& file_handle = _file_handles[i]; + auto* buf = _bufs[i]; + + std::future fut; + if (_config.per_file_pool) { + fut = file_handle->pread(buf, + _config.num_bytes, + 0, + defaults::task_size(), + defaults::gds_threshold(), + true, + _thread_pools[i].get()); + } else { + fut = file_handle->pread(buf, _config.num_bytes); + } + futs.push_back(std::move(fut)); + } + + for (auto&& fut : futs) { + fut.get(); + } +} + +std::size_t PosixBenchmark::nbytes_impl() { return _config.num_bytes * _config.filepaths.size(); } +} // namespace kvikio::benchmark + +int main(int argc, char* argv[]) +{ + try { + kvikio::benchmark::PosixConfig config; + config.parse_args(argc, argv); + kvikio::benchmark::PosixBenchmark bench(std::move(config)); + bench.run(); + } catch (std::exception const& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + return 0; +} diff --git a/cpp/benchmarks/local/posix_benchmark.hpp b/cpp/benchmarks/local/posix_benchmark.hpp new file mode 100644 index 0000000000..6d78ce8dc4 --- /dev/null +++ b/cpp/benchmarks/local/posix_benchmark.hpp @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "../utils.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace kvikio::benchmark { + +struct PosixConfig : Config { + bool overwrite_file{false}; + bool per_file_pool{false}; + + virtual void parse_args(int argc, char** argv) override; + virtual void print_usage(std::string const& program_name) override; +}; + +class PosixBenchmark : public Benchmark { + friend class Benchmark; + + protected: + std::vector> _file_handles; + std::vector _bufs; + std::vector> _thread_pools; + + void initialize_impl(); + void cleanup_impl(); + void run_target_impl(); + std::size_t nbytes_impl(); + + public: + PosixBenchmark(PosixConfig config); + ~PosixBenchmark(); +}; +} // namespace kvikio::benchmark diff --git a/cpp/benchmarks/utils.cpp b/cpp/benchmarks/utils.cpp new file mode 100644 index 0000000000..af85ec3556 --- /dev/null +++ b/cpp/benchmarks/utils.cpp @@ -0,0 +1,167 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "utils.hpp" + +#include + +#include +#include +#include + +namespace kvikio::benchmark { + +std::size_t parse_size(std::string const& str) +{ + if (str.empty()) { throw std::invalid_argument("Empty size string"); } + + // Parse the numeric part + std::size_t pos{}; + double value{}; + try { + value = std::stod(str, &pos); + } catch (std::exception const& e) { + throw std::invalid_argument("Invalid size format: " + str); + } + + if (value < 0) { throw std::invalid_argument("Size cannot be negative"); } + + // Extract suffix (everything after the number) + auto suffix = str.substr(pos); + + // No suffix means raw bytes + if (suffix.empty()) { return static_cast(value); } + + // Normalize to uppercase for case-insensitive comparison + std::transform( + suffix.begin(), suffix.end(), suffix.begin(), [](unsigned char c) { return std::tolower(c); }); + + // All multipliers use 1024 (binary), not 1000 + std::size_t multiplier{1}; + + // Support both K/Ki, M/Mi, etc. as synonyms (all 1024-based) + std::size_t constexpr one_Ki{1024ULL}; + std::size_t constexpr one_Mi{1024ULL * one_Ki}; + std::size_t constexpr one_Gi{1024ULL * one_Mi}; + std::size_t constexpr one_Ti{1024ULL * one_Gi}; + if (suffix == "k" || suffix == "ki" || suffix == "kib") { + multiplier = one_Ki; + } else if (suffix == "m" || suffix == "mi" || suffix == "mib") { + multiplier = one_Mi; + } else if (suffix == "g" || suffix == "gi" || suffix == "gib") { + multiplier = one_Gi; + } else if (suffix == "t" || suffix == "ti" || suffix == "tib") { + multiplier = one_Ti; + } else { + throw std::invalid_argument("Invalid size suffix: '" + suffix + + "' (use K/Ki/KiB, M/Mi/MiB, G/Gi/GiB, or T/Ti/TiB)"); + } + + return static_cast(value * multiplier); +} + +void Config::parse_args(int argc, char** argv) +{ + static option long_options[] = { + {"file", required_argument, nullptr, 'f'}, + {"size", required_argument, nullptr, 's'}, + {"threads", required_argument, nullptr, 't'}, + {"repetitions", required_argument, nullptr, 'r'}, + {"no-direct", no_argument, nullptr, 'D'}, + {"no-align", no_argument, nullptr, 'A'}, + {"drop-cache", no_argument, nullptr, 'c'}, + // {"overwrite", no_argument, nullptr, 'w'}, + {"open-once", no_argument, nullptr, 'o'}, + {"help", no_argument, nullptr, 'h'}, + {0, 0, 0, 0} // Sentinel to mark the end of the array. Needed by getopt_long() + }; + + int opt{0}; + int option_index{-1}; + + // - By default getopt_long() returns '?' to indicate errors if an option has missing argument or + // if an unknown option is encountered. The starting ':' in the optstring modifies this behavior. + // Missing argument error now causes the return value to be ':'. Unknow option still leads to '?' + // and its processing is deferred. + // - "f:" means option "-f" takes an argument "c" means option + // - "-c" does not take an argument + while ((opt = getopt_long(argc, argv, ":f:s:t:r:DAcoh", long_options, &option_index)) != -1) { + switch (opt) { + case 'f': { + filepaths.push_back(optarg); + break; + } + case 's': { + num_bytes = parse_size(optarg); // Helper to parse "1G", "500M", etc. + break; + } + case 't': { + num_threads = std::stoul(optarg); + break; + } + case 'r': { + repetition = std::stoi(optarg); + break; + } + case 'D': { + o_direct = false; + break; + } + case 'A': { + align_buffer = false; + break; + } + case 'c': { + drop_file_cache = true; + break; + } + case 'o': { + open_file_once = true; + break; + } + case 'h': { + print_usage(argv[0]); + std::exit(0); + break; + } + case ':': { + // The parsed option has missing argument + std::stringstream ss; + ss << "Missing argument for option " << argv[optind - 1] << " (-" + << static_cast(optopt) << ")"; + throw std::runtime_error(ss.str()); + break; + } + default: { + // Unknown option is deferred to subsequent parsing, if any + break; + } + } + } + + // Validation + if (filepaths.empty()) { throw std::invalid_argument("--file is required"); } + + // Reset getopt state for second pass in the future + optind = 1; +} + +void Config::print_usage(std::string const& program_name) +{ + std::cout << "Usage: " << program_name << " [OPTIONS]\n\n" + << "Options:\n" + << " -f, --file PATH File path to benchmark (required)\n" + << " -s, --size SIZE Number of bytes to read (default: 4G)\n" + << " Supports suffixes: K, M, G, T\n" + << " -t, --threads NUM Number of threads (default: 1)\n" + << " -r, --repetitions NUM Number of repetitions (default: 5)\n" + << " -D, --no-direct Disable O_DIRECT (use buffered I/O)\n" + << " -A, --no-align Disable buffer alignment\n" + << " -c, --drop-cache Drop page cache before each run\n" + << " -o, --open-once Open file once (not per iteration)\n" + << " -h, --help Show this help message\n"; +} + +} // namespace kvikio::benchmark diff --git a/cpp/benchmarks/utils.hpp b/cpp/benchmarks/utils.hpp new file mode 100644 index 0000000000..bd6ee47e7e --- /dev/null +++ b/cpp/benchmarks/utils.hpp @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace kvikio::benchmark { +// Helper to parse size strings like "1GiB", "1Gi", "1G". +std::size_t parse_size(std::string const& str); + +struct Config { + std::size_t num_bytes{4ull * 1024ull * 1024ull * 1024ull}; + std::vector filepaths; + bool align_buffer{true}; + bool o_direct{true}; + bool drop_file_cache{false}; + bool compat_mode{true}; + unsigned int num_threads{1}; + bool open_file_once{false}; + int repetition{5}; + + virtual void parse_args(int argc, char** argv); + virtual void print_usage(std::string const& program_name); +}; + +template +class Benchmark { + protected: + ConfigType _config; + + void initialize() { static_cast(this)->initialize_impl(); } + void cleanup() { static_cast(this)->cleanup_impl(); } + void run_target() { static_cast(this)->run_target_impl(); } + std::size_t nbytes() { return static_cast(this)->nbytes_impl(); } + + public: + Benchmark(ConfigType config) : _config(std::move(config)) + { + defaults::set_thread_pool_nthreads(_config.num_threads); + auto compat_mode = _config.compat_mode ? CompatMode::ON : CompatMode::OFF; + defaults::set_compat_mode(compat_mode); + } + + void run() + { + if (_config.open_file_once) { initialize(); } + + decltype(_config.repetition) count{0}; + double time_elapsed_total_us{0.0}; + for (decltype(_config.repetition) idx = 0; idx < _config.repetition; ++idx) { + if (_config.drop_file_cache) { kvikio::clear_page_cache(); } + + if (!_config.open_file_once) { initialize(); } + + auto start = std::chrono::steady_clock::now(); + run_target(); + auto end = std::chrono::steady_clock::now(); + + std::chrono::duration time_elapsed = end - start; + double time_elapsed_us = time_elapsed.count(); + if (idx > 0) { + ++count; + time_elapsed_total_us += time_elapsed_us; + } + double bandwidth = nbytes() / time_elapsed_us * 1e6 / 1024.0 / 1024.0; + std::cout << std::string(4, ' ') << std::left << std::setw(4) << idx << std::setw(10) + << bandwidth << " [MiB/s]" << std::endl; + + if (!_config.open_file_once) { cleanup(); } + } + double average_bandwidth = nbytes() * count / time_elapsed_total_us * 1e6 / 1024.0 / 1024.0; + std::cout << std::string(4, ' ') << "Average bandwidth: " << std::setw(10) << average_bandwidth + << " [MiB/s]" << std::endl; + + if (_config.open_file_once) { cleanup(); } + } +}; +} // namespace kvikio::benchmark