Skip to content

Instantly share code, notes, and snippets.

@sgn00
Last active August 31, 2025 06:58
Show Gist options
  • Select an option

  • Save sgn00/2314ec8055f8ddaae4981d969ec22ab8 to your computer and use it in GitHub Desktop.

Select an option

Save sgn00/2314ec8055f8ddaae4981d969ec22ab8 to your computer and use it in GitHub Desktop.
Pipe RTT ping-pong benchmark
// g++ ping_pong.cpp -o ping_pong -std=c++20 -O3
#include <algorithm>
#include <cerrno>
#include <cstdint>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <vector>
#include <chrono>
#include <fcntl.h>
#include <sched.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
static void die(const char* what) {
std::cerr << what << "\n";
std::exit(1);
}
static void print_help(const char* prog) {
std::cout << "Usage: " << prog << " [options] [iterations] [msg_size] [parent_cpu] [child_cpu]\n\n"
<< "Options:\n"
<< " -b Use blocking pipes (default is non-blocking)\n"
<< " -h Show this help message and exit\n\n"
<< "Arguments:\n"
<< " iterations Number of ping-pong iterations (default: 10000)\n"
<< " msg_size Message size in bytes (default: 64)\n"
<< " parent_cpu CPU to pin parent process to (default: 0)\n"
<< " child_cpu CPU to pin child process to (default: 1)\n\n"
<< "Examples:\n"
<< " " << prog << " # Non-blocking, defaults (10k iters, 64B, CPU0/CPU1)\n"
<< " " << prog << " -b 20000 256 2 3 # Blocking mode, 20k iters, 256B, CPUs 2/3\n";
}
static void pin_to_cpu(int cpu) {
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
if (sched_setaffinity(0, sizeof(set), &set) != 0) {
die("sched_setaffinity failed");
}
}
static void set_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) {
die("fcntl(F_GETFL) failed");
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
die("fcntl(F_SETFL O_NONBLOCK) failed");
}
}
static ssize_t write_full(int fd, const uint8_t* buf, size_t len) {
size_t off = 0;
while (off < len) {
ssize_t n = ::write(fd, buf + off, len - off);
if (n > 0) {
off += static_cast<size_t>(n);
} else if (n < 0) {
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
}
return -1;
}
}
return static_cast<ssize_t>(off);
}
static ssize_t read_full(int fd, uint8_t* buf, size_t len) {
size_t off = 0;
while (off < len) {
ssize_t n = ::read(fd, buf + off, len - off);
if (n > 0) {
off += static_cast<size_t>(n);
} else if (n == 0) {
return 0; // EOF
} else if (n < 0) {
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
}
return -1;
}
}
return static_cast<ssize_t>(off);
}
static inline uint64_t now_ns() {
using clock = std::chrono::steady_clock;
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
clock::now().time_since_epoch()
).count()
);
}
int main(int argc, char** argv) {
bool blocking_mode = false;
int arg_index = 1;
for (; arg_index < argc && argv[arg_index][0] == '-'; ++arg_index) {
std::string flag = argv[arg_index];
if (flag == "-b") {
blocking_mode = true;
} else if (flag == "-h") {
print_help(argv[0]);
return 0;
} else {
std::cerr << "Unknown option: " << flag << "\n";
print_help(argv[0]);
return 1;
}
}
int iterations = (argc > arg_index) ? std::max(1, std::atoi(argv[arg_index])) : 10000;
int msg_size = (argc > arg_index + 1) ? std::max(1, std::atoi(argv[arg_index + 1])) : 64;
int parent_cpu = (argc > arg_index + 2) ? std::atoi(argv[arg_index + 2]) : 0;
int child_cpu = (argc > arg_index + 3) ? std::atoi(argv[arg_index + 3]) : 1;
long ncpu = sysconf(_SC_NPROCESSORS_CONF);
if (ncpu > 0) {
if (parent_cpu < 0 || child_cpu < 0 || parent_cpu >= ncpu || child_cpu >= ncpu) {
std::cerr << "CPU ids must be in [0," << ncpu << ")\n";
return 1;
}
}
int p2c[2];
int c2p[2];
if (pipe(p2c) != 0) {
die("pipe p2c failed");
}
if (pipe(c2p) != 0) {
die("pipe c2p failed");
}
if (!blocking_mode) {
set_nonblock(p2c[0]);
set_nonblock(p2c[1]);
set_nonblock(c2p[0]);
set_nonblock(c2p[1]);
}
pid_t child = fork();
if (child < 0) {
die("fork failed");
}
if (child == 0) {
close(p2c[1]);
close(c2p[0]);
pin_to_cpu(child_cpu);
std::vector<uint8_t> buf(static_cast<size_t>(msg_size));
for (int i = 0; i < iterations; ++i) {
if (read_full(p2c[0], buf.data(), buf.size()) <= 0) {
die("child read failed");
}
if (write_full(c2p[1], buf.data(), buf.size()) <= 0) {
die("child write failed");
}
}
close(p2c[0]);
close(c2p[1]);
std::exit(0);
}
close(p2c[0]);
close(c2p[1]);
pin_to_cpu(parent_cpu);
std::vector<uint8_t> tx(static_cast<size_t>(msg_size), 0);
std::vector<uint8_t> rx(static_cast<size_t>(msg_size), 0);
for (int i = 0; i < msg_size; ++i) {
tx[static_cast<size_t>(i)] = static_cast<uint8_t>(i);
}
uint64_t t0 = now_ns();
for (int i = 0; i < iterations; ++i) {
if (write_full(p2c[1], tx.data(), tx.size()) <= 0) {
die("parent write failed");
}
if (read_full(c2p[0], rx.data(), rx.size()) <= 0) {
die("parent read failed");
}
}
uint64_t t1 = now_ns();
double avg_ns = static_cast<double>(t1 - t0) / static_cast<double>(iterations);
std::cout << "Mode: " << (blocking_mode ? "Blocking" : "Non-blocking") << "\n";
std::cout << "Iterations: " << iterations
<< ", Msg: " << msg_size << " bytes, Parent CPU: " << parent_cpu
<< ", Child CPU: " << child_cpu << "\n";
std::cout << "Average RTT: " << avg_ns << " ns (" << (avg_ns / 1000.0) << " us)\n";
close(p2c[1]);
close(c2p[0]);
int st = 0;
waitpid(child, &st, 0);
return st == 0 ? 0 : 1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment