Skip to content

Commit

Permalink
Benchmark charts.
Browse files Browse the repository at this point in the history
Utility to write external bstree insertion functions.
Return false when inserting duplicates.
  • Loading branch information
voxelstack committed Aug 19, 2024
1 parent ec86d41 commit ffbfea7
Show file tree
Hide file tree
Showing 16 changed files with 346 additions and 24 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ctest.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: ctest
on: push
env:
NO_BENCHMARKS: 1
NO_DOCS: 1

jobs:
Expand Down
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ set(CMAKE_C_STANDARD 99)

add_subdirectory("include")

if(NOT DEFINED ENV{NO_BENCHMARKS})
add_subdirectory("benchmarks")
endif()

if(NOT DEFINED ENV{NO_DOCS})
add_subdirectory("docs")
endif()
Expand Down
9 changes: 9 additions & 0 deletions benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
include(Benchmarks)

add_benchmark(ds/bstree.c)

add_chart(
SOURCE ds/bstree.c
NAME bstree_insert_fnptr
RUNS fnptr nofnptr
)
37 changes: 37 additions & 0 deletions benchmarks/benchmarks.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#pragma once

#include <stdio.h>
#include <windows.h>

#include <perflib.h>

#define _RUNS 100

#define setup() \
LARGE_INTEGER _t0; \
LARGE_INTEGER _t1; \
LARGE_INTEGER _f; \
long long _ts[_RUNS];

#define start() QueryPerformanceFrequency(&_f);

#define benchmark(f) \
printf("%s,", #f); \
f(); \
printf("%lld", _ts[0]); \
for (int _i = 1; _i < _RUNS; ++_i) \
printf(";%lld", _ts[_i]); \
printf("\n");

#define time_start() \
for (int _i = 0; _i < _RUNS; ++_i) \
{ \
QueryPerformanceCounter(&_t0);

#define time_end() \
QueryPerformanceCounter(&_t1); \
_ts[_i] = (long long)((_t1.QuadPart - _t0.QuadPart) * 1000.0 \
/ _f.QuadPart); \
}

#define end() return 0;
108 changes: 108 additions & 0 deletions benchmarks/ds/bstree.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#include "../benchmarks.h"

#include <ds/bstree.h>

setup();

int
main()
{
start();

benchmark(fnptr);
benchmark(nofnptr);

end();
}

struct holder
{
int data;
struct bstree bst;
};

void
insert_fnptr(struct bstree** root, struct bstree* n, int (*cmp)(void*, void*))
{
struct bstree** link = root;
struct bstree* parent = NULL;
while (*link)
{
parent = *link;
// Allowing duplicates will not affect the benchmark.
if (cmp(n, *link) < 0)
link = &((*link)->_left);
else
link = &((*link)->_right);
}

bstree_link(link, n, parent);
}

void
insert_nofnptr(struct bstree** root, struct holder* n)
{
struct bstree** link = root;
struct bstree* parent = NULL;
while (*link)
{
struct holder* this = container_of(*link, struct holder, bst);
int result = n->data - this->data;

parent = *link;
// Allowing duplicates will not affect the benchmark.
if (result < 0)
link = &((*link)->_left);
else
link = &((*link)->_right);
}

bstree_link(link, &n->bst, parent);
}

int
comparator(void* a, void* b)
{
return container_of(a, struct holder, bst)->data
- container_of(b, struct holder, bst)->data;
}

int
fnptr()
{
int n = 1000000;
struct holder* nodes = malloc(n * sizeof(struct holder));
struct bstree* root;

for (int i = 0; i < n; ++i)
nodes[i].data = rand();

time_start();
root = NULL;
for (int i = 0; i < n; ++i)
insert_fnptr(&root, &nodes[i].bst, comparator);
time_end();

free(nodes);
return 0;
}

int
nofnptr()
{
int n = 1000000;
struct holder* nodes = malloc(n * sizeof(struct holder));
struct bstree* root;

for (int i = 0; i < n; ++i)
nodes[i].data = rand();

time_start();
root = NULL;
for (int i = 0; i < n; ++i)
insert_nofnptr(&root, &nodes[i]);
time_end();

free(nodes);
return 0;
}
64 changes: 64 additions & 0 deletions cmake/Benchmarks.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
find_package(Python COMPONENTS Interpreter)

add_custom_target(benchmarks)
add_custom_target(charts)
set(BENCHMARK_CHART "${CMAKE_SOURCE_DIR}/scripts/benchmark_chart.py")
set(BENCHMARK_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/results")
file(MAKE_DIRECTORY ${BENCHMARK_OUTPUT_DIR})

function(add_benchmark SOURCE)
string(REPLACE ".c" "" TARGET ${SOURCE})
string(REPLACE "/" "." TARGET ${TARGET})
string(PREPEND TARGET "bench.")

set(RUNNER "${TARGET}.runner")

add_executable(${TARGET} ${SOURCE})
target_link_libraries(${TARGET} PRIVATE leet)
target_compile_options(${TARGET} PRIVATE "-O0")
# Benchmark macros execute functions that are defined at the end of the file.
# Benchmark functions always return int and have no arguments.
target_compile_options(${TARGET} PRIVATE "-Wno-implicit-function-declaration")

set(BENCHMARK_BINARY "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}.exe")
set(BENCHMARK_CSV "${BENCHMARK_OUTPUT_DIR}/${TARGET}.csv")
add_custom_command(
OUTPUT ${BENCHMARK_CSV}
DEPENDS ${TARGET} ${SOURCE}
COMMAND
${BENCHMARK_BINARY} > ${BENCHMARK_CSV}
WORKING_DIRECTORY ${BENCHMARK_OUTPUT_DIR}
)
add_custom_target(${RUNNER} DEPENDS ${BENCHMARK_CSV})
add_dependencies(benchmarks ${RUNNER})
endfunction()

function(add_chart)
set(oneValueArgs SOURCE NAME)
set(multiValueArgs RUNS)
cmake_parse_arguments(
CHART
""
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN}
)

string(REPLACE ".c" "" TARGET ${CHART_SOURCE})
string(REPLACE "/" "." TARGET ${TARGET})
set(CHART ${TARGET})
string(PREPEND CHART "chart.")
string(PREPEND TARGET "bench.")

set(BENCHMARK_CSV "${BENCHMARK_OUTPUT_DIR}/${TARGET}.csv")
set(CHART_JSON "${CMAKE_SOURCE_DIR}/docs/_charts/bench.${CHART_NAME}.json")
add_custom_command(
OUTPUT ${CHART_JSON}
DEPENDS ${BENCHMARK_CSV}
COMMAND
${Python_EXECUTABLE} ${BENCHMARK_CHART}
${BENCHMARK_CSV} ${CHART_JSON} ${CHART_RUNS}
)
add_custom_target(${CHART} DEPENDS ${CHART_JSON})
add_dependencies(charts ${CHART})
endfunction()
4 changes: 3 additions & 1 deletion docs/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ find_package(Sphinx REQUIRED)

file(GLOB_RECURSE LEET_PUBLIC_HEADERS ${PROJECT_SOURCE_DIR}/include/*.h)
file(GLOB_RECURSE LEET_DOC_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.rst)
file(GLOB_RECURSE LEET_CHART_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/_charts/*.json)

set(DOXYGEN_INPUT_DIR ${PROJECT_SOURCE_DIR}/include)
set(DOXYGEN_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/doxygen)
Expand Down Expand Up @@ -37,8 +38,9 @@ add_custom_command(
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS
${LEET_DOC_SOURCES}
${LEET_CHART_SOURCES}
${DOXYGEN_INDEX_FILE}
MAIN_DEPENDENCY ${SPHINX_SOURCE}/conf.py
COMMENT "Generating sphinx html."
)
add_custom_target(Sphinx ALL DEPENDS ${SPHINX_INDEX_FILE})
add_custom_target(Sphinx ALL DEPENDS ${SPHINX_INDEX_FILE} charts)
4 changes: 4 additions & 0 deletions docs/_charts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# \_charts

`bench.*.json` files are generated from benchmarks.
These files are needed for the Read the Docs build but we don't want to run benchmarks on Read the Docs servers, which is why they are being committed.
1 change: 1 addition & 0 deletions docs/_charts/bench.bstree_insert_fnptr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"data": [{"x": [1161, 1097, 1114, 1139, 1124, 1189, 1117, 1113, 1102, 1100, 1126, 1096, 1156, 1095, 1081, 1079, 1083, 1080, 1112, 1134, 1362, 1199, 1242, 1105, 1089, 1132, 1196, 1283, 1150, 1107, 1099, 1118, 1165, 1152, 1140, 1130, 1122, 1126, 1104, 1138, 1090, 1100, 1148, 1083, 1101, 1100, 1086, 1141, 1135, 1091, 1152, 1231, 1273, 1172, 1108, 1116, 1116, 1128, 1114, 1087, 1114, 1103, 1068, 1118, 1092, 1153, 1124, 1074, 1200, 1272, 1199, 1096, 1112, 1137, 1120, 1098, 1090, 1092, 1150, 1128, 1151, 1109, 1135, 1103, 1108, 1120, 1076, 1113, 1128, 1096, 1105, 1089, 1095, 1113, 1092, 1151, 1122, 1109, 1084, 1102], "line": {"color": "#2980b9"}, "type": "box", "name": "fnptr", "orientation": "h", "width": 0.15}, {"x": [1075, 1074, 1119, 1102, 1068, 1104, 1136, 1085, 1070, 1105, 1091, 1101, 1093, 1061, 1039, 1077, 1089, 1057, 1096, 1111, 1091, 1080, 1070, 1085, 1077, 1066, 1081, 1096, 1090, 1324, 1114, 1145, 1071, 1087, 1119, 1299, 1084, 1191, 1082, 1074, 1068, 1052, 1098, 1057, 1087, 1092, 1104, 1058, 1085, 1110, 1094, 1097, 1086, 1075, 1054, 1072, 1095, 1101, 1102, 1064, 1101, 1086, 1107, 1076, 1087, 1098, 1047, 1101, 1066, 1099, 1080, 1048, 1064, 1100, 1067, 1085, 1081, 1061, 1097, 1071, 1056, 1098, 1056, 1081, 1100, 1177, 1229, 1143, 1111, 1065, 1095, 1302, 1194, 1191, 1083, 1085, 1100, 1100, 1044, 1062], "line": {"color": "#2980b9"}, "type": "box", "name": "nofnptr", "orientation": "h", "width": 0.15}], "layout": {"showlegend": false, "xaxis": {"title": {"text": "Time (ms)"}, "type": "linear"}, "yaxis": {"type": "category"}, "autosize": true}}
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = ["breathe", "sphinx.ext.todo"]
extensions = ["breathe", "sphinx.ext.todo", "sphinx_charts.charts"]

# Breathe configuration
breathe_default_project = "leet"
Expand Down
10 changes: 10 additions & 0 deletions docs/lib/ds/bstree.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Binary search tree
==================

Inserting data into a bstree
----------------------------
There are two main ways to insert data into the tree: using the provided insertion function along with a comparator, or writing your own insertion function.
Writing your own insertion function is more performant since it avoids using a function pointer.

.. chart:: _charts/bench.bstree_insert_fnptr.json

1.000.000 random numbers inserted into a bstree (-O0, 100 runs)

API
---

Expand All @@ -16,6 +25,7 @@ ______
Functions
_________

.. doxygenfunction:: bstree_link
.. doxygenfunction:: bstree_insert
.. doxygenfunction:: bstree_search
.. doxygenfunction:: bstree_first
Expand Down
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
breathe
sphinx-rtd-theme
sphinx_charts
55 changes: 35 additions & 20 deletions include/ds/bstree.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@ struct bstree
*/
void _transplant(struct bstree** root, struct bstree* u, struct bstree* v);

/**
* @brief Grafts a new node into the tree at the given location.
*
* Utility for writing external insert functions. Writing your own insert
* function is more performant since it avoids using a function pointer.
*
* @param link Location to insert the node.
* @param n The node to insert.
* @param parent Parent of the node to insert.
*/
void
bstree_link(struct bstree** link, struct bstree* n, struct bstree* parent)
{
n->_parent = parent;
n->_left = n->_right = NULL;
*link = n;
}

/**
* @brief Inserts a node at the appropriate position on the tree.
*
Expand All @@ -56,30 +74,27 @@ void _transplant(struct bstree** root, struct bstree* u, struct bstree* v);
* @param n The node to insert.
* @param cmp Insertion comparator.
*/
void
bool
bstree_insert(struct bstree** root, struct bstree* n, int (*cmp)(void*, void*))
{
struct bstree* x = *root;
struct bstree* y = NULL;
while (x)
struct bstree** link = root;
struct bstree* parent = NULL;
while (*link)
{
// Invariant: x is not empty, the new node must be inserted to
// the left or to the right.
y = x;
if (cmp(n, x) < 0)
x = x->_left;
// Invariant: link is not empty, the new node must be inserted
// to the left or to the right.
parent = *link;
int result = cmp(n, *link);
if (result < 0)
link = &((*link)->_left);
else if (result > 0)
link = &((*link)->_right);
else
x = x->_right;
return false;
}

n->_parent = y;
if (!y)
// The tree was empty.
*root = n;
else if (cmp(n, y) < 0)
y->_left = n;
else
y->_right = n;
bstree_link(link, n, parent);
return true;
}

/**
Expand Down Expand Up @@ -143,7 +158,7 @@ bstree_last(struct bstree* n)
*
* Used to traverse the tree in order.
*
* `for (struct bstree* it = bstree_first(&root); it; it = bstree_next(it))`
* `for (struct bstree* it = bstree_first(root); it; it = bstree_next(it))`
*
* @param n Handle to the current node.
* @return Handle to the successor of the node.
Expand All @@ -169,7 +184,7 @@ bstree_next(struct bstree* n)
*
* Used to traverse the tree in reverse order.
*
* `for (struct bstree* it = bstree_last(&root); it; it = bstree_prev(it))`
* `for (struct bstree* it = bstree_last(root); it; it = bstree_prev(it))`
*
* @param n Handle to the current node.
* @return Handle to the predecessor of the node.
Expand Down
Loading

0 comments on commit ffbfea7

Please sign in to comment.