io_uring vs pread: Random Reads at Queue Depth 32
How many random 4 KiB reads per second can one thread sustain with serial pread() syscalls, compared with io_uring submitting 32 at a time, when the page cache is taken out of the picture?
Results
| Repeats | 3 |
|---|---|
| O Direct | true |
| Pread Iops | 5567 |
| Block Bytes | 4096 |
| Blocks Read | 20000 |
| Queue Depth | 32 |
| Io Uring Iops | 41433 |
| Pread Best Seconds | 3.5923 |
| Io Uring Best Seconds | 0.4827 |
| Pread Mean Latency Us | 179.62 |
| Io Uring Speedup Factor | 7.44 |
Recorded September 8, 2026 at 5:01 AM UTC, wall clock 13.2s.
Method
A 512 MiB file is read with O_DIRECT, so neither path can be served from the page cache. Both paths read the same 20,000 block offsets in the same order, generated from a fixed hash so the comparison is not affected by which blocks happen to be luckier. The pread path issues one blocking syscall per block. The io_uring path submits up to 32 reads per io_uring_enter and reaps completions in batch, single-threaded, no SQPOLL and no registered buffers. Both are warmed up at one twentieth of the workload, then each is run three times alternately and reported by its fastest run. The difference measured is the cost of crossing into the kernel once per read versus once per batch.
Machine
| CPU | AMD EPYC 9354P 32-Core Processor |
|---|---|
| Cores visible | 8 |
| Memory | 31.3 GB |
| Kernel | 6.8.0-139-generic |
| Architecture | x64 |
| Compiler | gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 |
This is a shared virtual server, not an isolated test rig. Absolute throughput will differ on your hardware; the ratio between the two cases is the part that carries over.
Source
The complete program that produced the numbers above. Nothing else was running under our control during the measurement.
/*
* Compares two ways of issuing the same random reads against one file:
*
* pread() one syscall per read, blocking, issued serially
* io_uring QD reads submitted per io_uring_enter, completions reaped in batch
*
* Both paths read the same offsets, the same block size, the same total number
* of blocks, from the same file, with O_DIRECT so the page cache cannot serve
* one path from memory while the other goes to disk. What differs is how many
* times the process crosses into the kernel.
*
* Build: gcc -O2 -o bench bench.c -luring
* Run: ./bench <file> <block_bytes> <blocks> <queue_depth>
* Output: one JSON object on stdout.
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <liburing.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static double now_seconds(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
/* Deterministic offsets, so both paths read exactly the same blocks in exactly
* the same order — otherwise the comparison measures luck, not mechanism. */
static uint64_t offset_for(uint64_t i, uint64_t block, uint64_t blocks_in_file) {
uint64_t x = i * 6364136223846793005ULL + 1442695040888963407ULL;
x ^= x >> 33;
return (x % blocks_in_file) * block;
}
static double run_pread(int fd, size_t block, uint64_t blocks, uint64_t blocks_in_file, void *buf) {
double start = now_seconds();
for (uint64_t i = 0; i < blocks; i++) {
if (pread(fd, buf, block, (off_t)offset_for(i, block, blocks_in_file)) < 0) {
perror("pread");
exit(1);
}
}
return now_seconds() - start;
}
static double run_uring(int fd, size_t block, uint64_t blocks, uint64_t blocks_in_file, void **bufs, unsigned qd) {
struct io_uring ring;
if (io_uring_queue_init(qd, &ring, 0) < 0) {
fprintf(stderr, "io_uring_queue_init failed\n");
exit(1);
}
double start = now_seconds();
uint64_t submitted = 0, completed = 0;
while (completed < blocks) {
unsigned batch = 0;
while (submitted < blocks && batch < qd) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
if (!sqe) break;
io_uring_prep_read(sqe, fd, bufs[batch], block,
(off_t)offset_for(submitted, block, blocks_in_file));
submitted++;
batch++;
}
if (batch > 0) io_uring_submit(&ring);
for (unsigned i = 0; i < batch; i++) {
struct io_uring_cqe *cqe;
if (io_uring_wait_cqe(&ring, &cqe) < 0) {
fprintf(stderr, "io_uring_wait_cqe failed\n");
exit(1);
}
if (cqe->res < 0) {
fprintf(stderr, "read failed: %s\n", strerror(-cqe->res));
exit(1);
}
io_uring_cqe_seen(&ring, cqe);
completed++;
}
}
double elapsed = now_seconds() - start;
io_uring_queue_exit(&ring);
return elapsed;
}
int main(int argc, char **argv) {
const char *path = argc > 1 ? argv[1] : "testfile.bin";
size_t block = argc > 2 ? (size_t)strtoull(argv[2], NULL, 10) : 4096;
uint64_t blocks = argc > 3 ? strtoull(argv[3], NULL, 10) : 20000;
unsigned qd = argc > 4 ? (unsigned)strtoul(argv[4], NULL, 10) : 32;
int fd = open(path, O_RDONLY | O_DIRECT);
if (fd < 0) {
perror("open");
return 1;
}
off_t size = lseek(fd, 0, SEEK_END);
uint64_t blocks_in_file = (uint64_t)size / block;
if (blocks_in_file == 0) {
fprintf(stderr, "file too small for block size\n");
return 1;
}
void *single;
if (posix_memalign(&single, 4096, block) != 0) return 1;
void **bufs = calloc(qd, sizeof(void *));
for (unsigned i = 0; i < qd; i++) {
if (posix_memalign(&bufs[i], 4096, block) != 0) return 1;
}
/* Warm up both paths before measuring. */
run_pread(fd, block, blocks / 20, blocks_in_file, single);
run_uring(fd, block, blocks / 20, blocks_in_file, bufs, qd);
const int repeats = 3;
double pread_best = 1e9, uring_best = 1e9;
for (int r = 0; r < repeats; r++) {
double a = run_pread(fd, block, blocks, blocks_in_file, single);
double b = run_uring(fd, block, blocks, blocks_in_file, bufs, qd);
if (a < pread_best) pread_best = a;
if (b < uring_best) uring_best = b;
}
printf("{\n");
printf(" \"block_bytes\": %zu,\n", block);
printf(" \"blocks_read\": %llu,\n", (unsigned long long)blocks);
printf(" \"queue_depth\": %u,\n", qd);
printf(" \"repeats\": %d,\n", repeats);
printf(" \"o_direct\": true,\n");
printf(" \"pread_best_seconds\": %.4f,\n", pread_best);
printf(" \"io_uring_best_seconds\": %.4f,\n", uring_best);
printf(" \"pread_iops\": %.0f,\n", (double)blocks / pread_best);
printf(" \"io_uring_iops\": %.0f,\n", (double)blocks / uring_best);
printf(" \"pread_mean_latency_us\": %.2f,\n", pread_best / (double)blocks * 1e6);
printf(" \"io_uring_speedup_factor\": %.2f\n", pread_best / uring_best);
printf("}\n");
close(fd);
return 0;
}
Previous runs
| Date | Headline result |
|---|---|
| 2026-09-08 05:01 | 7.44 |
| 2026-09-08 04:56 | 7.8 |