Add benchmark and readme
This commit is contained in:
241
README.md
Normal file
241
README.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Rope Data Structure (C++ Implementation)
|
||||
|
||||
This library provides an efficient **rope** implementation in C++ for large, mutable text.
|
||||
It supports fast insertion, deletion, concatenation, iteration, and regex-based search.
|
||||
The design uses fixed-size leaf chunks and automatically maintains a balanced binary tree.
|
||||
|
||||
## Features
|
||||
|
||||
* Efficient handling of large text buffers
|
||||
* Log-time insertion, deletion, and concatenation
|
||||
* Optional tunable chunk sizes
|
||||
* Line, leaf, and byte iterators
|
||||
* Regex search using PCRE2 (DFA mode)
|
||||
* Regex using `noose` can also be done but it is much slower for now, I am working on making it faster.
|
||||
* Zero-copy leaf iteration (safe immutable reads)
|
||||
* Fully heap-allocated, manual memory management
|
||||
* Deterministic balancing strategy (AVL-style depth tracking)
|
||||
|
||||
## ⚡ Performance Benchmarks
|
||||
|
||||
Performance comparison between this Rope implementation and the standard C++ `std::string`.
|
||||
|
||||
**Test Context:**
|
||||
* **Dataset:** ~1 GiB generated text (1,073,741,856 bytes).
|
||||
* **Goal:** Simulating heavy text-editing operations.
|
||||
|
||||
### Key Highlights
|
||||
When handling large datasets, the structural advantages of the Rope become massive:
|
||||
|
||||
* **Appending Large Text:** **2.4 Million x faster** (0.001ms vs 1831ms)
|
||||
* **Splitting Text:** **162,000x faster**
|
||||
* **Insert (Middle):** **87,000x faster**
|
||||
* **Erase:** **15,000x faster**
|
||||
|
||||
### Numbers
|
||||
|
||||
| Operation | Rope Time | String Time | String to Rope Ratio | Winner |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **Concat (Append Large)** | **0.001 ms** | 1831.666 ms | **2,429,265x** | ✅ **Rope** |
|
||||
| **Split (Half)** | **0.008 ms** | 1353.086 ms | **162,240x** | ✅ **Rope** |
|
||||
| **Insert (Middle)** | **0.011 ms** | 976.105 ms | **87,206x** | ✅ **Rope** |
|
||||
| **Erase (5KB)** | **0.013 ms** | 207.760 ms | **15,953x** | ✅ **Rope** |
|
||||
| **Iterate 1000 Lines** | **0.063 ms** | 5.723 ms | **90x** | ✅ **Rope** |
|
||||
| **Concat (Append small)** | 0.002 ms | **0.000 ms** | 0.1 | ❌ String |
|
||||
| **Load / Create** | 1512.449 ms | **871.330 ms** | 0.58x | ❌ String |
|
||||
| **Free / Destruct** | 194.167 ms | **153.365 ms** | 0.79x | ❌ String |
|
||||
| **Read / Substr (1KiB)** | 0.008 ms | **0.002 ms** | 0.24x | ❌ String |
|
||||
| **Search (Regex)** | 6417.719 ms | **1.526 ms** | ~0x | ❌ String |
|
||||
|
||||
|
||||
**Why is the Rope faster?**<br>
|
||||
Standard strings require contiguous memory. When you insert text into the middle of a 1GB `std::string`, the CPU must shift all subsequent bytes in memory ($O(n)$ complexity). This Rope implementation uses a tree structure, allowing insertions and deletions by simply modifying pointers ($O(\log n)$ complexity).
|
||||
|
||||
Also for concatenation ropes have amortized constant time vs strings with linear time.<br>
|
||||
So smaller concatenation strings are faster but for any larger concatenation the rope is best.<br>
|
||||
|
||||
|
||||
**Why is the String faster for Regex/Read?**<br>
|
||||
`std::string` is just a flat array of bytes, which is extremely cache-friendly for linear scanning and regex engines. Ropes require tree traversal to read sequential data, resulting in higher overhead for read-only operations.
|
||||
|
||||
I have also tested against `ropey` the rust library and rust strings.<br>
|
||||
Ropey has much faster load times but is slightly slower than my implementation in other operations.<br>
|
||||
I am not including the tests here as my test might not be in the most optimal condition and so have different results.<br>
|
||||
The tests included here are the ones that I have done myself with clang++ compiling both tests in the same binary. (see: `tests/benchmark.cpp`)<br>
|
||||
Also on a side note rust strings are faster than c++ strings apart from reading.
|
||||
|
||||
|
||||
## Rope Node Structure
|
||||
|
||||
Each rope node (`Knot`) is either:
|
||||
|
||||
* an **internal node** with `left` and `right` children, or
|
||||
* a **leaf node** containing text data of size `chunk_size`.
|
||||
|
||||
Metadata stored per node:
|
||||
|
||||
* `depth` – subtree height
|
||||
* `chunk_size` – leaf capacity
|
||||
* `line_count` – number of `\n` newline characters
|
||||
* `char_count` – total byte length covered by the subtree
|
||||
|
||||
```c
|
||||
typedef struct Knot {
|
||||
Knot *left;
|
||||
Knot *right;
|
||||
uint8_t depth;
|
||||
uint32_t chunk_size;
|
||||
uint32_t line_count;
|
||||
uint32_t char_count;
|
||||
char data[];
|
||||
} Knot;
|
||||
```
|
||||
|
||||
|
||||
## Chunk Size
|
||||
|
||||
The library supports arbitrary positive chunk sizes, but provides:
|
||||
|
||||
```c
|
||||
uint32_t optimal_chunk_size(uint64_t length);
|
||||
```
|
||||
|
||||
This suggests a chunk size based on the target input size.
|
||||
Valid range is:
|
||||
|
||||
* **MIN_CHUNK_SIZE:** 64 bytes
|
||||
* **MAX_CHUNK_SIZE:** 8192 bytes
|
||||
|
||||
You may choose any value; all ropes participating in concat operations **must use the same chunk size**.
|
||||
|
||||
|
||||
# API Overview
|
||||
|
||||
## Construction and Loading
|
||||
|
||||
```c
|
||||
load(char *str, uint32_t len, uint32_t chunk_size)
|
||||
```
|
||||
|
||||
Builds a rope from a raw byte buffer.
|
||||
`str` is not consumed and may be freed after loading.
|
||||
|
||||
|
||||
## Structural Operations
|
||||
|
||||
```c
|
||||
Knot *concat(Knot *left, Knot *right)
|
||||
```
|
||||
|
||||
Concatenates two ropes (must share chunk size).
|
||||
Both input roots are invalid after the call.
|
||||
|
||||
```c
|
||||
Knot *insert(Knot *node, uint32_t offset, char *str, uint32_t len)
|
||||
```
|
||||
|
||||
Inserts text at a byte offset.
|
||||
Returns a new rope.
|
||||
The original node becomes invalid.
|
||||
|
||||
```c
|
||||
Knot *erase(Knot *node, uint32_t offset, uint32_t len)
|
||||
```
|
||||
|
||||
Deletes a byte range.
|
||||
Returns a new rope.
|
||||
The original node becomes invalid.
|
||||
|
||||
```c
|
||||
void split(Knot *node, uint32_t offset, Knot **left, Knot **right)
|
||||
```
|
||||
|
||||
Splits a rope into two ropes at the given byte offset.
|
||||
The original node becomes invalid.
|
||||
|
||||
```c
|
||||
char *read(Knot *root, uint32_t offset, uint32_t len)
|
||||
```
|
||||
|
||||
Extracts a substring.
|
||||
Returns a null-terminated buffer; caller must free it.
|
||||
|
||||
|
||||
## Line & Byte Mapping
|
||||
|
||||
```c
|
||||
uint32_t byte_to_line(Knot *root, uint32_t offset)
|
||||
```
|
||||
|
||||
Converts a byte offset into a line index.
|
||||
|
||||
```c
|
||||
uint32_t line_to_byte(Knot *root, uint32_t line, uint32_t *out_len)
|
||||
```
|
||||
|
||||
Returns the byte offset of the beginning of a line and outputs that line’s length.
|
||||
|
||||
|
||||
## Iterators
|
||||
|
||||
### Line Iterator
|
||||
|
||||
```c
|
||||
LineIterator *begin_l_iter(Knot *root, uint32_t start_line);
|
||||
char *next_line(LineIterator *it); // caller frees result
|
||||
```
|
||||
|
||||
### Leaf Iterator
|
||||
|
||||
```c
|
||||
LeafIterator *begin_k_iter(Knot *root);
|
||||
char *next_leaf(LeafIterator *it); // DO NOT free result
|
||||
```
|
||||
|
||||
### Byte Iterator
|
||||
|
||||
```c
|
||||
ByteIterator *begin_b_iter(Knot *root);
|
||||
char next_byte(ByteIterator *it);
|
||||
```
|
||||
|
||||
All iterators must be freed by the caller after use.
|
||||
> For `ByteIterator`, `ByteIterator.it` must be freed before the iterator is freed.
|
||||
|
||||
|
||||
## Searching
|
||||
|
||||
```cpp
|
||||
std::vector<std::pair<size_t, size_t>> search_rope(Knot *root, const char *pattern)
|
||||
```
|
||||
|
||||
Searches the rope using PCRE2 in DFA mode.
|
||||
Returns a vector of `(start_offset, length)` pairs.
|
||||
Only deterministic patterns are supported (no backtracking).
|
||||
|
||||
|
||||
## Memory Management
|
||||
|
||||
```c
|
||||
void free_rope(Knot *root)
|
||||
```
|
||||
|
||||
Recursively frees all nodes in the rope.
|
||||
Must be called once when the rope is no longer needed.
|
||||
|
||||
|
||||
# Example Usage
|
||||
|
||||
```c
|
||||
uint32_t chunk_size = optimal_chunk_size(strlen(input));
|
||||
Knot *r = load(input, strlen(input), chunk_size);
|
||||
|
||||
r = insert(r, 5, "hello", 5);
|
||||
r = erase(r, 20, 3);
|
||||
|
||||
char *sub = read(r, 10, 25);
|
||||
printf("%s\n", sub);
|
||||
free(sub);
|
||||
|
||||
free_rope(r);
|
||||
```
|
||||
@@ -44,7 +44,7 @@ Knot *load(char *str, uint32_t len, uint32_t chunk_size) {
|
||||
node->line_count = left->line_count + right->line_count;
|
||||
return node;
|
||||
} else {
|
||||
Knot *node = (Knot *)malloc(sizeof(Knot) + chunk_size);
|
||||
Knot *node = (Knot *)malloc(sizeof(Knot) + chunk_size + 1);
|
||||
if (!node)
|
||||
return nullptr;
|
||||
node->left = nullptr;
|
||||
@@ -67,7 +67,7 @@ Knot *load(char *str, uint32_t len, uint32_t chunk_size) {
|
||||
// leaf if consumed and freed (so dont use or free it after)
|
||||
// left and right are the new nodes
|
||||
static void split_leaf(Knot *leaf, uint32_t k, Knot **left, Knot **right) {
|
||||
Knot *left_node = (Knot *)malloc(sizeof(Knot) + leaf->chunk_size);
|
||||
Knot *left_node = (Knot *)malloc(sizeof(Knot) + leaf->chunk_size + 1);
|
||||
left_node->left = nullptr;
|
||||
left_node->right = nullptr;
|
||||
left_node->chunk_size = leaf->chunk_size;
|
||||
@@ -83,7 +83,7 @@ static void split_leaf(Knot *leaf, uint32_t k, Knot **left, Knot **right) {
|
||||
left_node->line_count = newline_count;
|
||||
uint16_t right_line_count = leaf->line_count - newline_count;
|
||||
*left = left_node;
|
||||
Knot *right_node = (Knot *)malloc(sizeof(Knot) + leaf->chunk_size);
|
||||
Knot *right_node = (Knot *)malloc(sizeof(Knot) + leaf->chunk_size + 1);
|
||||
right_node->left = nullptr;
|
||||
right_node->right = nullptr;
|
||||
right_node->chunk_size = leaf->chunk_size;
|
||||
@@ -191,7 +191,7 @@ Knot *concat(Knot *left, Knot *right) {
|
||||
}
|
||||
if (left->depth == 0 && right->depth == 0) {
|
||||
if (left->char_count + right->char_count <= left->chunk_size) {
|
||||
Knot *node = (Knot *)malloc(sizeof(Knot) + left->chunk_size);
|
||||
Knot *node = (Knot *)malloc(sizeof(Knot) + left->chunk_size + 1);
|
||||
node->left = nullptr;
|
||||
node->right = nullptr;
|
||||
node->chunk_size = left->chunk_size;
|
||||
|
||||
299
src/test.cpp
299
src/test.cpp
@@ -1,299 +0,0 @@
|
||||
#include "../api/noose.hpp"
|
||||
#include "../api/rope.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
char *load_file(const char *path, size_t *out_len) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
perror("fopen");
|
||||
return nullptr;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
size_t len = ftell(f);
|
||||
rewind(f);
|
||||
|
||||
char *buf = (char *)malloc(len);
|
||||
if (!buf) {
|
||||
perror("malloc");
|
||||
fclose(f);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
fread(buf, 1, len, f);
|
||||
fclose(f);
|
||||
|
||||
*out_len = len;
|
||||
return buf;
|
||||
}
|
||||
|
||||
int main() {
|
||||
|
||||
printf("My rope implementation benchmark\n");
|
||||
|
||||
{
|
||||
size_t len;
|
||||
printf("Loading file into rope...\n");
|
||||
char *buf = load_file("./random.bin", &len);
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
Knot *root = load(buf, len, optimal_chunk_size(len));
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
printf("Load time: %.3f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
free(buf);
|
||||
|
||||
// READ TEST
|
||||
printf("Testing read...\n");
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
char *content = read(root, len / 2, 1024);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
free(content);
|
||||
printf("Read 1 KB from middle: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
// INSERT TEST
|
||||
printf("Testing insert...\n");
|
||||
char insert_data[1024];
|
||||
memset(insert_data, 'X', 1024);
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
root = insert(root, len / 2, insert_data, 1024);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
printf("Insert 1 KB in middle: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
// ERASE TEST (Delete the same 1 KB we just inserted)
|
||||
printf("Testing erase...\n");
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
root = erase(root, len / 2, 1024);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
printf("Erase 1 KB in middle: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
// SPLIT TEST
|
||||
printf("Testing split...\n");
|
||||
Knot *left = nullptr, *right = nullptr;
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
split(root, len / 2, &left, &right);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
printf("Split at middle: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
// CONCAT TEST
|
||||
printf("Testing concat...\n");
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
root = concat(left, right);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
printf("Concat: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// LINE OPERATIONS TESTS
|
||||
// ---------------------------------------------------------
|
||||
printf("Testing line operations...\n");
|
||||
|
||||
// KNOWN CONSTANTS based on: yes "The quick brown fox jumps over the lazy
|
||||
// dog." String length: 44 + 1 newline = 45 bytes per line.
|
||||
const uint32_t BYTES_PER_LINE = 45;
|
||||
const uint32_t TEST_LINE_INDEX = 1000; // A line deep in the file
|
||||
|
||||
// 1. Test byte_to_line
|
||||
// We pick a byte in the middle of TEST_LINE_INDEX.
|
||||
// Offset = (100000 * 45) + 10.
|
||||
uint32_t test_offset = (TEST_LINE_INDEX * BYTES_PER_LINE) + 10;
|
||||
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
uint16_t calculated_line = byte_to_line(root, test_offset);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
printf("byte_to_line (%u -> %u): %.6f s ", test_offset, calculated_line,
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
if (calculated_line == TEST_LINE_INDEX) {
|
||||
printf("[PASS]\n");
|
||||
} else {
|
||||
printf("[FAIL] Expected %u, got %u\n", TEST_LINE_INDEX, calculated_line);
|
||||
}
|
||||
|
||||
// 2. Test line_to_byte
|
||||
// We ask for the start of TEST_LINE_INDEX. Should be exactly
|
||||
// TEST_LINE_INDEX * 45.
|
||||
uint32_t out_len = 0;
|
||||
uint32_t expected_start = TEST_LINE_INDEX * BYTES_PER_LINE;
|
||||
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
uint32_t calculated_start = line_to_byte(root, TEST_LINE_INDEX, &out_len);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
printf("line_to_byte (Line %u -> Offset %u): %.6f s ", TEST_LINE_INDEX,
|
||||
calculated_start,
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
if (calculated_start == expected_start && out_len == BYTES_PER_LINE) {
|
||||
printf("[PASS]\n");
|
||||
} else {
|
||||
printf("[FAIL] Expected offset %u (len %u), got %u (len %u)\n",
|
||||
expected_start, BYTES_PER_LINE, calculated_start, out_len);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// ITERATOR SPEED TEST
|
||||
// ---------------------------------------------------------
|
||||
printf("Testing iterator speed...\n");
|
||||
|
||||
const uint32_t LINES_TO_ITERATE = 10000; // Iterate 10,000 lines
|
||||
|
||||
// 1. Initialize the iterator at a deep line index
|
||||
uint32_t start_line = TEST_LINE_INDEX + 10;
|
||||
|
||||
LeafIterator *it = begin_k_iter(root);
|
||||
if (!it) {
|
||||
printf("Iterator Test: [FAIL] begin_iterator returned NULL.\n");
|
||||
} else {
|
||||
char *line = NULL;
|
||||
uint32_t lines_read = 0;
|
||||
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// 2. Iterate and time the process
|
||||
// We use the clean C idiom: get the line, check for NULL, then
|
||||
// process.
|
||||
while (lines_read < LINES_TO_ITERATE && (line = next_leaf(it)) != NULL) {
|
||||
// Note: We deliberately skip printing to focus on the Rope operation
|
||||
// time.
|
||||
lines_read++;
|
||||
}
|
||||
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double elapsed_time = std::chrono::duration<double>(end - start).count();
|
||||
|
||||
printf("Iterator speed (f:: %u): %.6f s (%.2f lines/s)\n", lines_read,
|
||||
elapsed_time, (double)lines_read / elapsed_time);
|
||||
|
||||
if (lines_read == LINES_TO_ITERATE) {
|
||||
printf("Iterator Test: [PASS] Successfully iterated %u lines.\n",
|
||||
LINES_TO_ITERATE);
|
||||
} else {
|
||||
printf("Iterator Test: [FAIL] Expected %u lines, read %u.\n",
|
||||
LINES_TO_ITERATE, lines_read);
|
||||
}
|
||||
|
||||
// 3. Clean up the iterator
|
||||
free(it);
|
||||
}
|
||||
|
||||
// search test
|
||||
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
std::vector<std::pair<size_t, size_t>> matches =
|
||||
search_rope(root, "[A-Z][a-z]+");
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
printf("Search Time: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
printf("Found %lu matches\n", matches.size());
|
||||
|
||||
// char *c = read(root, 0, 1000);
|
||||
// printf("%s\n", c);
|
||||
// free(c);
|
||||
|
||||
// ByteIterator *it1 = begin_b_iter(root);
|
||||
// char ch;
|
||||
// while ((ch = next_byte(it1)) != '\0') {
|
||||
// printf("%c:", ch);
|
||||
// }
|
||||
|
||||
ByteIterator *it2 = begin_b_iter(root);
|
||||
uint32_t saved[40];
|
||||
for (int i = 0; i < 40; i++)
|
||||
saved[i] = 0;
|
||||
std::string pattern = "[A-Z][a-z]+";
|
||||
Inst *program = compile_regex(pattern);
|
||||
print_program(program);
|
||||
bool result;
|
||||
int prolen = proglen(program);
|
||||
ThreadList *clist = (ThreadList *)malloc(sizeof(ThreadList));
|
||||
clist->t = (Thread *)malloc(+sizeof(Thread) * prolen);
|
||||
clist->n = 0;
|
||||
ThreadList *nlist = (ThreadList *)malloc(sizeof(ThreadList));
|
||||
nlist->t = (Thread *)malloc(+sizeof(Thread) * prolen);
|
||||
nlist->n = 0;
|
||||
int count = 0;
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
while ((result = next_match(program, it2, saved, clist, nlist))) {
|
||||
count++;
|
||||
}
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
printf("Search Time: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
printf("Found2 %d matches\n", count);
|
||||
|
||||
free_program(program);
|
||||
free(it2->it);
|
||||
free(it2);
|
||||
free(clist->t);
|
||||
free(nlist->t);
|
||||
free(clist);
|
||||
free(nlist);
|
||||
|
||||
free_rope(root);
|
||||
}
|
||||
|
||||
printf("Testing std::string...\n");
|
||||
|
||||
{
|
||||
std::ifstream file("random.bin", std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
perror("ifstream");
|
||||
return 1;
|
||||
}
|
||||
size_t len = file.tellg();
|
||||
file.seekg(0);
|
||||
std::string data(len, '\0');
|
||||
file.read(data.data(), len);
|
||||
|
||||
std::string s = data;
|
||||
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
// READ: middle 1 KB
|
||||
std::string read_chunk = s.substr(len / 2, 1024);
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
printf("std::string read 1 KB from middle: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
// INSERT: middle 1 KB
|
||||
std::string insert_data(1024, 'X');
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
s.insert(len / 2, insert_data);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
printf("std::string insert 1 KB in middle: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
// ERASE: middle 1 KB
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
s.erase(len / 2, 1024);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
printf("std::string erase 1 KB in middle: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
// SPLIT: middle
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
std::string left = s.substr(0, len / 2);
|
||||
std::string right = s.substr(len / 2);
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
printf("std::string split at middle: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
|
||||
// CONCAT
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
s = left + right;
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
printf("std::string concat: %.6f s\n",
|
||||
std::chrono::duration<double>(end - start).count());
|
||||
}
|
||||
}
|
||||
342
tests/benchmark.cpp
Normal file
342
tests/benchmark.cpp
Normal file
@@ -0,0 +1,342 @@
|
||||
#include "../api/rope.hpp"
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
|
||||
// Include the user's header
|
||||
|
||||
// --- Timer Helper ---
|
||||
class Timer {
|
||||
using Clock = std::chrono::high_resolution_clock;
|
||||
std::chrono::time_point<Clock> start_time;
|
||||
|
||||
public:
|
||||
Timer() { reset(); }
|
||||
void reset() { start_time = Clock::now(); }
|
||||
double elapsed_ms() {
|
||||
auto end_time = Clock::now();
|
||||
return std::chrono::duration<double, std::milli>(end_time - start_time)
|
||||
.count();
|
||||
}
|
||||
};
|
||||
|
||||
// --- Formatting Helper ---
|
||||
void print_result(const std::string &test_name, double rope_ms, double str_ms) {
|
||||
std::cout << std::left << std::setw(25) << test_name
|
||||
<< " | Rope: " << std::setw(10) << std::fixed
|
||||
<< std::setprecision(3) << rope_ms << " ms"
|
||||
<< " | String: " << std::setw(10) << str_ms << " ms"
|
||||
<< " | Ratio (Str/Rope): " << std::setprecision(2) << std::setw(10)
|
||||
<< (str_ms / rope_ms) << "x" << " | " << std::fixed << " So "
|
||||
<< ((str_ms - rope_ms) <= 0 ? "string" : "rope ")
|
||||
<< " is faster by " << std::fabs(str_ms - rope_ms) << " ms"
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
int main() {
|
||||
// 1. DATA GENERATION
|
||||
std::cout << "Generating ~1GiB dataset..." << std::endl;
|
||||
const std::string pattern = "The quick brown fox jumps over the lzy dog.\n";
|
||||
// Target ~100 MiB (100 * 1024 * 1024 bytes)
|
||||
const size_t target_size = 1024 * 1024 * 1024;
|
||||
std::string source_data;
|
||||
source_data.reserve(target_size + pattern.size());
|
||||
|
||||
while (source_data.size() < target_size)
|
||||
source_data.append(pattern);
|
||||
|
||||
uint32_t total_len = static_cast<uint32_t>(source_data.size());
|
||||
std::cout << "Dataset generated. Size: " << total_len << " bytes.\n"
|
||||
<< std::endl;
|
||||
|
||||
Timer t;
|
||||
double rope_time, str_time;
|
||||
|
||||
// ==========================================
|
||||
// TEST 1: LOAD / CREATION
|
||||
// ==========================================
|
||||
|
||||
// Rope Load
|
||||
t.reset();
|
||||
uint32_t chunk_size = optimal_chunk_size(total_len);
|
||||
// Note: Cast to char* because header asks for char*, usually strings are
|
||||
// const char*
|
||||
Knot *root =
|
||||
load(const_cast<char *>(source_data.c_str()), total_len, chunk_size);
|
||||
rope_time = t.elapsed_ms();
|
||||
|
||||
// String Load (Copy)
|
||||
t.reset();
|
||||
std::string str_copy = source_data;
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Load / Create", rope_time, str_time);
|
||||
|
||||
// ==========================================
|
||||
// TEST 2: INSERT (Middle)
|
||||
// ==========================================
|
||||
std::string insert_pattern = " [INSERTED TEXT] ";
|
||||
uint32_t insert_pos = total_len / 2;
|
||||
|
||||
// Rope Insert
|
||||
t.reset();
|
||||
root = insert(root, insert_pos, const_cast<char *>(insert_pattern.c_str()),
|
||||
(uint32_t)insert_pattern.size());
|
||||
rope_time = t.elapsed_ms();
|
||||
|
||||
// String Insert
|
||||
t.reset();
|
||||
str_copy.insert(insert_pos, insert_pattern);
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Insert (Middle)", rope_time, str_time);
|
||||
|
||||
// ==========================================
|
||||
// TEST 3: READ / SUBSTR
|
||||
// ==========================================
|
||||
uint32_t read_len = 1024;
|
||||
uint32_t read_pos = total_len / 2; // Read from where we just inserted
|
||||
|
||||
// Rope Read
|
||||
t.reset();
|
||||
char *rope_read_res = read(root, read_pos, read_len);
|
||||
rope_time = t.elapsed_ms();
|
||||
free(rope_read_res); // Free result as per header
|
||||
|
||||
// String Substr
|
||||
t.reset();
|
||||
std::string str_read_res = str_copy.substr(read_pos, read_len);
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Read / Substr (1KiB)", rope_time, str_time);
|
||||
|
||||
// ==========================================
|
||||
// TEST 4: CONCATENATION
|
||||
// ==========================================
|
||||
// Create a temporary rope to append
|
||||
Knot *suffix_rope = load(const_cast<char *>(pattern.c_str()),
|
||||
(uint32_t)pattern.size(), chunk_size);
|
||||
|
||||
// Rope Concat
|
||||
t.reset();
|
||||
root = concat(root, suffix_rope);
|
||||
rope_time = t.elapsed_ms();
|
||||
|
||||
// String Append
|
||||
t.reset();
|
||||
str_copy += pattern;
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Concat (Append small)", rope_time, str_time);
|
||||
|
||||
Knot *large_rope =
|
||||
load(const_cast<char *>(source_data.c_str()), total_len, chunk_size);
|
||||
|
||||
// Rope Concat
|
||||
t.reset();
|
||||
root = concat(root, large_rope);
|
||||
rope_time = t.elapsed_ms();
|
||||
|
||||
Knot *L = nullptr;
|
||||
Knot *R = nullptr;
|
||||
split(root, total_len, &L, &R);
|
||||
root = L;
|
||||
free_rope(R);
|
||||
|
||||
// String Append
|
||||
t.reset();
|
||||
str_copy += source_data;
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Concat (Append large)", rope_time, str_time);
|
||||
|
||||
// ==========================================
|
||||
// TEST 5: ERASE
|
||||
// ==========================================
|
||||
uint32_t erase_len = 5000; // Erase 5KB
|
||||
uint32_t erase_pos = total_len / 4;
|
||||
|
||||
// Rope Erase
|
||||
t.reset();
|
||||
root = erase(root, erase_pos, erase_len);
|
||||
rope_time = t.elapsed_ms();
|
||||
|
||||
// String Erase
|
||||
t.reset();
|
||||
str_copy.erase(erase_pos, erase_len);
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Erase (5KB)", rope_time, str_time);
|
||||
|
||||
// ==========================================
|
||||
// TEST 6: LINE TO BYTE (Indexing)
|
||||
// ==========================================
|
||||
// Pick a line number deep in the file
|
||||
uint32_t target_line = 100000;
|
||||
uint32_t out_len = 0;
|
||||
|
||||
// Rope Line Lookup
|
||||
t.reset();
|
||||
volatile uint32_t r_offset = line_to_byte(root, target_line, &out_len);
|
||||
rope_time = t.elapsed_ms();
|
||||
|
||||
// String Line Lookup (Simulated: Must scan for newlines)
|
||||
t.reset();
|
||||
size_t current_line = 0;
|
||||
size_t s_offset = 0;
|
||||
// Manual scan is the standard way for std::string
|
||||
for (size_t i = 0; i < str_copy.size(); ++i) {
|
||||
if (str_copy[i] == '\n') {
|
||||
current_line++;
|
||||
if (current_line == target_line) {
|
||||
s_offset = i + 1; // Start of next line
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Line -> Byte Offset", rope_time, str_time);
|
||||
|
||||
// ==========================================
|
||||
// TEST 7: BYTE TO LINE
|
||||
// ==========================================
|
||||
uint32_t target_offset = total_len / 2;
|
||||
|
||||
// Rope Byte Lookup
|
||||
t.reset();
|
||||
volatile uint32_t r_line = byte_to_line(root, target_offset);
|
||||
rope_time = t.elapsed_ms();
|
||||
|
||||
// String Byte Lookup (Simulated scan backwards or from start)
|
||||
t.reset();
|
||||
size_t s_line = 0;
|
||||
for (size_t i = 0; i < target_offset && i < str_copy.size(); ++i) {
|
||||
if (str_copy[i] == '\n')
|
||||
s_line++;
|
||||
}
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Byte Offset -> Line", rope_time, str_time);
|
||||
|
||||
// ==========================================
|
||||
// TEST 8: LINE ITERATION (Next 1000 lines)
|
||||
// ==========================================
|
||||
int lines_to_read = 1000;
|
||||
uint32_t start_iter_line = 50000;
|
||||
|
||||
// Rope Iteration
|
||||
t.reset();
|
||||
LineIterator *lit = begin_l_iter(root, start_iter_line);
|
||||
for (int i = 0; i < lines_to_read; ++i) {
|
||||
char *line = next_line(lit);
|
||||
if (line)
|
||||
free(line); // Must free per header
|
||||
else
|
||||
break;
|
||||
}
|
||||
// Note: Assuming `free(lit)` or similar is needed,
|
||||
// though header says "returned iterator must be freed".
|
||||
// I will assume standard `delete` or `free` works on the struct pointer.
|
||||
free(lit);
|
||||
rope_time = t.elapsed_ms();
|
||||
|
||||
// String Iteration
|
||||
// To be fair, we find the starting offset, then read lines
|
||||
t.reset();
|
||||
size_t iter_offset = 0;
|
||||
size_t cur_ln = 0;
|
||||
// Fast forward (cost of finding start)
|
||||
while (cur_ln < start_iter_line && iter_offset < str_copy.size()) {
|
||||
if (str_copy[iter_offset++] == '\n')
|
||||
cur_ln++;
|
||||
}
|
||||
// Read loop
|
||||
for (int i = 0; i < lines_to_read && iter_offset < str_copy.size(); ++i) {
|
||||
size_t next_nl = str_copy.find('\n', iter_offset);
|
||||
if (next_nl == std::string::npos)
|
||||
break;
|
||||
// Simulate extracting the string
|
||||
volatile std::string temp =
|
||||
str_copy.substr(iter_offset, next_nl - iter_offset);
|
||||
iter_offset = next_nl + 1;
|
||||
}
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Iterate 1000 Lines", rope_time, str_time);
|
||||
|
||||
// ==========================================
|
||||
// TEST 9: SEARCH (Regex)
|
||||
// ==========================================
|
||||
// Search for a specific pattern that occurs
|
||||
const char *search_pattern = "brown fox";
|
||||
|
||||
// Rope Search (DFA/PCRE as per header)
|
||||
t.reset();
|
||||
auto rope_matches = search_rope(root, search_pattern);
|
||||
rope_time = t.elapsed_ms();
|
||||
|
||||
t.reset();
|
||||
try {
|
||||
std::regex re(search_pattern);
|
||||
auto words_begin =
|
||||
std::sregex_iterator(str_copy.begin(), str_copy.end(), re);
|
||||
auto words_end = std::sregex_iterator();
|
||||
size_t count = 0;
|
||||
for (std::sregex_iterator i = words_begin; i != words_end; ++i) {
|
||||
count++;
|
||||
// Don't iterate millions of times for the benchmark if it takes forever
|
||||
if (count > 1000)
|
||||
break;
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Search (Regex)", rope_time, str_time);
|
||||
|
||||
// ==========================================
|
||||
// TEST 10: SPLIT
|
||||
// ==========================================
|
||||
uint32_t split_point = total_len / 2;
|
||||
Knot *left_side = nullptr;
|
||||
Knot *right_side = nullptr;
|
||||
|
||||
// Rope Split
|
||||
t.reset();
|
||||
// split consumes 'root', so root is invalid after this
|
||||
split(root, split_point, &left_side, &right_side);
|
||||
rope_time = t.elapsed_ms();
|
||||
|
||||
// String Split (Simulated via substr copies)
|
||||
t.reset();
|
||||
std::string s_left = str_copy.substr(0, split_point);
|
||||
std::string s_right = str_copy.substr(split_point);
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Split (Half)", rope_time, str_time);
|
||||
|
||||
// ==========================================
|
||||
// CLEANUP
|
||||
// ==========================================
|
||||
t.reset();
|
||||
free_rope(left_side);
|
||||
free_rope(right_side);
|
||||
rope_time = t.elapsed_ms();
|
||||
|
||||
// std::string cleans up automatically, but let's time the destruction
|
||||
t.reset();
|
||||
{
|
||||
std::string temp1 = std::move(s_left);
|
||||
std::string temp2 = std::move(s_right);
|
||||
} // destructors run here
|
||||
str_time = t.elapsed_ms();
|
||||
|
||||
print_result("Free / Destruct", rope_time, str_time);
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user