Last active
June 8, 2026 00:26
-
-
Save fffaraz/a1940bb0f449615624d884f3883ddf28 to your computer and use it in GitHub Desktop.
Memory Benchmark
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // g++ -O3 -std=c++17 benchmark.cpp -o benchmark | |
| #include <iostream> | |
| #include <vector> | |
| #include <chrono> | |
| #include <iomanip> | |
| #include <cstdint> | |
| #include <string> | |
| #include <sstream> | |
| #include <random> | |
| #include <algorithm> | |
| using namespace std; | |
| // Helper to format output sizes | |
| string formatSize(size_t bytes) { | |
| const size_t KB = 1024; | |
| const size_t MB = 1024 * KB; | |
| const size_t GB = 1024 * MB; | |
| stringstream ss; | |
| if (bytes >= GB) { | |
| ss << (bytes / GB) << " GB"; | |
| } else if (bytes >= MB) { | |
| ss << (bytes / MB) << " MB"; | |
| } else { | |
| ss << (bytes / KB) << " KB"; | |
| } | |
| return ss.str(); | |
| } | |
| int main() { | |
| const size_t KB = 1024; | |
| const size_t MB = 1024 * KB; | |
| const size_t GB = 1024 * MB; | |
| // Start at 4KB, double until 4GB | |
| vector<size_t> sizes; | |
| for (size_t s = 4 * KB; s <= 4ULL * GB; s *= 2) { | |
| sizes.push_back(s); | |
| } | |
| // Increased target bytes to 32 GB for sufficient iterations on the largest arrays | |
| const size_t TARGET_BYTES = 32ULL * GB; | |
| const size_t LATENCY_ITERATIONS = 20000000; // Baseline pointer chases | |
| const size_t STRIDE_BYTES = 64; | |
| cout << setw(15) << left << "Buffer Size" | |
| << setw(20) << left << "Bandwidth (GB/s)" | |
| << "Latency (ns)" << "\n"; | |
| cout << "--------------------------------------------------------\n"; | |
| mt19937_64 rng(123456789); | |
| for (size_t size : sizes) { | |
| // Use size_t for the buffer to cleanly support index chasing | |
| size_t num_elements = size / sizeof(size_t); | |
| vector<size_t> buffer(num_elements); | |
| // ========================================== | |
| // BANDWIDTH MEASUREMENT (Sequential Strided) | |
| // ========================================== | |
| // Fill buffer with pseudo-random data | |
| uint32_t state = 123456789; | |
| for (size_t i = 0; i < num_elements; ++i) { | |
| state ^= state << 13; | |
| state ^= state >> 17; | |
| state ^= state << 5; | |
| buffer[i] = static_cast<size_t>(state); | |
| } | |
| size_t bw_iterations = TARGET_BYTES / size; | |
| if (bw_iterations == 0) bw_iterations = 1; | |
| size_t accumulator = 0; | |
| size_t elements_per_stride = STRIDE_BYTES / sizeof(size_t); | |
| // Warm-up pass | |
| for (size_t j = 0; j < num_elements; j += elements_per_stride) { | |
| accumulator += buffer[j]; | |
| } | |
| auto start_bw = chrono::high_resolution_clock::now(); | |
| for (size_t i = 0; i < bw_iterations; ++i) { | |
| for (size_t j = 0; j < num_elements; j += elements_per_stride) { | |
| accumulator += buffer[j]; | |
| } | |
| } | |
| auto end_bw = chrono::high_resolution_clock::now(); | |
| chrono::duration<double> duration_bw = end_bw - start_bw; | |
| volatile size_t sink_bw = accumulator; | |
| double bytes_read = static_cast<double>(bw_iterations) * (size / STRIDE_BYTES) * STRIDE_BYTES; | |
| double gb_per_sec = (bytes_read / static_cast<double>(GB)) / duration_bw.count(); | |
| // ========================================== | |
| // LATENCY MEASUREMENT (Random Chasing) | |
| // ========================================== | |
| size_t num_cache_lines = size / STRIDE_BYTES; | |
| vector<size_t> cycle(num_cache_lines); | |
| for (size_t i = 0; i < num_cache_lines; ++i) { | |
| cycle[i] = i; | |
| } | |
| // Sattolo's algorithm for a single, unbroken permutation cycle | |
| for (size_t i = num_cache_lines - 1; i > 0; --i) { | |
| uniform_int_distribution<size_t> dist(0, i - 1); | |
| size_t j = dist(rng); | |
| swap(cycle[i], cycle[j]); | |
| } | |
| // Write the cycle links directly into the stride boundaries of the buffer | |
| for (size_t i = 0; i < num_cache_lines; ++i) { | |
| buffer[i * elements_per_stride] = cycle[i] * elements_per_stride; | |
| } | |
| size_t current_index = 0; | |
| // Warm-up pass for latency | |
| for (size_t i = 0; i < num_cache_lines; ++i) { | |
| current_index = buffer[current_index]; | |
| } | |
| // Scale iterations so tiny buffers don't finish too quickly to measure | |
| size_t lat_iterations = max(num_cache_lines * 2, LATENCY_ITERATIONS); | |
| auto start_lat = chrono::high_resolution_clock::now(); | |
| // The dependency chain: CPU cannot fetch the next index until the current load completes | |
| for (size_t i = 0; i < lat_iterations; ++i) { | |
| current_index = buffer[current_index]; | |
| } | |
| auto end_lat = chrono::high_resolution_clock::now(); | |
| chrono::duration<double, std::nano> duration_lat = end_lat - start_lat; | |
| // Prevent compiler from optimizing away the loop | |
| volatile size_t sink_lat = current_index; | |
| double latency_ns = duration_lat.count() / static_cast<double>(lat_iterations); | |
| cout << setw(15) << left << formatSize(size) | |
| << setw(20) << left << fixed << setprecision(3) << gb_per_sec | |
| << fixed << setprecision(2) << latency_ns << "\n"; | |
| } | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment