Skip to content
Draft
2 changes: 2 additions & 0 deletions cpp/benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
179 changes: 179 additions & 0 deletions cpp/benchmarks/local/posix_benchmark.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/

#include "posix_benchmark.hpp"
#include "../utils.hpp"

#include <fcntl.h>

#include <iostream>
#include <memory>
#include <sstream>
#include <stdexcept>

#include <kvikio/compat_mode.hpp>
#include <kvikio/defaults.hpp>
#include <kvikio/detail/utils.hpp>
#include <kvikio/error.hpp>
#include <kvikio/file_handle.hpp>
#include <kvikio/file_utils.hpp>
#include <kvikio/threadpool_wrapper.hpp>

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<char>(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<kvikio::BS_thread_pool>(_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<kvikio::FileHandle>(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<std::future<std::size_t>> futs;

for (std::size_t i = 0; i < _file_handles.size(); ++i) {
auto& file_handle = _file_handles[i];
auto* buf = _bufs[i];

std::future<std::size_t> 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();
}
}
} // 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;
}
44 changes: 44 additions & 0 deletions cpp/benchmarks/local/posix_benchmark.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/

#pragma once

#include "../utils.hpp"

#include <getopt.h>
#include <memory>
#include <string>
#include <vector>

#include <kvikio/file_handle.hpp>
#include <kvikio/threadpool_wrapper.hpp>

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<PosixBenchmark, PosixConfig> {
friend class Benchmark<PosixBenchmark, PosixConfig>;

protected:
std::vector<std::unique_ptr<kvikio::FileHandle>> _file_handles;
std::vector<void*> _bufs;
std::vector<std::unique_ptr<kvikio::BS_thread_pool>> _thread_pools;

void initialize_impl();
void cleanup_impl();
void run_target_impl();

public:
PosixBenchmark(PosixConfig config);
~PosixBenchmark();
};
} // namespace kvikio::benchmark
167 changes: 167 additions & 0 deletions cpp/benchmarks/utils.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/

#include "utils.hpp"

#include <getopt.h>

#include <algorithm>
#include <sstream>
#include <stdexcept>

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<std::size_t>(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<std::size_t>(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<char>(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
Loading