Skip to content

Commit 28284af

Browse files
committed
Benchmark charts.
Utility to write external bstree insertion functions. Return false when inserting duplicates.
1 parent ec86d41 commit 28284af

File tree

16 files changed

+346
-24
lines changed

16 files changed

+346
-24
lines changed

.github/workflows/ctest.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: ctest
22
on: push
33
env:
4+
NO_BENCHMARKS: 1
45
NO_DOCS: 1
56

67
jobs:

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ set(CMAKE_C_STANDARD 99)
77

88
add_subdirectory("include")
99

10+
if(NOT DEFINED ENV{NO_BENCHMARKS})
11+
add_subdirectory("benchmarks")
12+
endif()
13+
1014
if(NOT DEFINED ENV{NO_DOCS})
1115
add_subdirectory("docs")
1216
endif()

benchmarks/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
include(Benchmarks)
2+
3+
add_benchmark(ds/bstree.c)
4+
5+
add_chart(
6+
SOURCE ds/bstree.c
7+
NAME bstree_insert_fnptr
8+
RUNS fnptr nofnptr
9+
)

benchmarks/benchmarks.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#pragma once
2+
3+
#include <stdio.h>
4+
#include <windows.h>
5+
6+
#include <perflib.h>
7+
8+
#define _RUNS 100
9+
10+
#define setup() \
11+
LARGE_INTEGER _t0; \
12+
LARGE_INTEGER _t1; \
13+
LARGE_INTEGER _f; \
14+
long long _ts[_RUNS];
15+
16+
#define start() QueryPerformanceFrequency(&_f);
17+
18+
#define benchmark(f) \
19+
printf("%s,", #f); \
20+
f(); \
21+
printf("%lld", _ts[0]); \
22+
for (int _i = 1; _i < _RUNS; ++_i) \
23+
printf(";%lld", _ts[_i]); \
24+
printf("\n");
25+
26+
#define time_start() \
27+
for (int _i = 0; _i < _RUNS; ++_i) \
28+
{ \
29+
QueryPerformanceCounter(&_t0);
30+
31+
#define time_end() \
32+
QueryPerformanceCounter(&_t1); \
33+
_ts[_i] = (long long)((_t1.QuadPart - _t0.QuadPart) * 1000.0 \
34+
/ _f.QuadPart); \
35+
}
36+
37+
#define end()

benchmarks/ds/bstree.c

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#include "../benchmarks.h"
2+
3+
#include <ds/bstree.h>
4+
5+
setup();
6+
7+
int
8+
main()
9+
{
10+
start();
11+
12+
benchmark(fnptr);
13+
benchmark(nofnptr);
14+
15+
end();
16+
}
17+
18+
struct holder
19+
{
20+
int data;
21+
struct bstree bst;
22+
};
23+
24+
void
25+
insert_fnptr(struct bstree** root, struct bstree* n, int (*cmp)(void*, void*))
26+
{
27+
struct bstree** link = root;
28+
struct bstree* parent = NULL;
29+
while (*link)
30+
{
31+
parent = *link;
32+
// Allowing duplicates will not affect the benchmark.
33+
if (cmp(n, *link) < 0)
34+
link = &((*link)->_left);
35+
else
36+
link = &((*link)->_right);
37+
}
38+
39+
bstree_link(link, n, parent);
40+
}
41+
42+
void
43+
insert_nofnptr(struct bstree** root, struct holder* n)
44+
{
45+
struct bstree** link = root;
46+
struct bstree* parent = NULL;
47+
while (*link)
48+
{
49+
struct holder* this = container_of(*link, struct holder, bst);
50+
int result = n->data - this->data;
51+
52+
parent = *link;
53+
// Allowing duplicates will not affect the benchmark.
54+
if (result < 0)
55+
link = &((*link)->_left);
56+
else
57+
link = &((*link)->_right);
58+
}
59+
60+
bstree_link(link, &n->bst, parent);
61+
}
62+
63+
int
64+
comparator(void* a, void* b)
65+
{
66+
return container_of(a, struct holder, bst)->data
67+
- container_of(b, struct holder, bst)->data;
68+
}
69+
70+
int
71+
fnptr()
72+
{
73+
int n = 1000000;
74+
struct holder* nodes = malloc(n * sizeof(struct holder));
75+
struct bstree* root;
76+
77+
for (int i = 0; i < n; ++i)
78+
nodes[i].data = rand();
79+
80+
time_start();
81+
root = NULL;
82+
for (int i = 0; i < n; ++i)
83+
insert_fnptr(&root, &nodes[i].bst, comparator);
84+
time_end();
85+
86+
free(nodes);
87+
return 0;
88+
}
89+
90+
int
91+
nofnptr()
92+
{
93+
int n = 1000000;
94+
struct holder* nodes = malloc(n * sizeof(struct holder));
95+
struct bstree* root;
96+
97+
for (int i = 0; i < n; ++i)
98+
nodes[i].data = rand();
99+
100+
time_start();
101+
root = NULL;
102+
for (int i = 0; i < n; ++i)
103+
insert_nofnptr(&root, &nodes[i]);
104+
time_end();
105+
106+
free(nodes);
107+
return 0;
108+
}

cmake/Benchmarks.cmake

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
find_package(Python COMPONENTS Interpreter)
2+
3+
add_custom_target(benchmarks)
4+
add_custom_target(charts)
5+
set(BENCHMARK_CHART "${CMAKE_SOURCE_DIR}/scripts/benchmark_chart.py")
6+
set(BENCHMARK_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/results")
7+
file(MAKE_DIRECTORY ${BENCHMARK_OUTPUT_DIR})
8+
9+
function(add_benchmark SOURCE)
10+
string(REPLACE ".c" "" TARGET ${SOURCE})
11+
string(REPLACE "/" "." TARGET ${TARGET})
12+
string(PREPEND TARGET "bench.")
13+
14+
set(RUNNER "${TARGET}.runner")
15+
16+
add_executable(${TARGET} ${SOURCE})
17+
target_link_libraries(${TARGET} PRIVATE leet)
18+
target_compile_options(${TARGET} PRIVATE "-O0")
19+
# Benchmark macros execute functions that are defined at the end of the file.
20+
# Benchmark functions always return int and have no arguments.
21+
target_compile_options(${TARGET} PRIVATE "-Wno-implicit-function-declaration")
22+
23+
set(BENCHMARK_BINARY "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}.exe")
24+
set(BENCHMARK_CSV "${BENCHMARK_OUTPUT_DIR}/${TARGET}.csv")
25+
add_custom_command(
26+
OUTPUT ${BENCHMARK_CSV}
27+
DEPENDS ${TARGET} ${SOURCE}
28+
COMMAND
29+
${BENCHMARK_BINARY} > ${BENCHMARK_CSV}
30+
WORKING_DIRECTORY ${BENCHMARK_OUTPUT_DIR}
31+
)
32+
add_custom_target(${RUNNER} DEPENDS ${BENCHMARK_CSV})
33+
add_dependencies(benchmarks ${RUNNER})
34+
endfunction()
35+
36+
function(add_chart)
37+
set(oneValueArgs SOURCE NAME)
38+
set(multiValueArgs RUNS)
39+
cmake_parse_arguments(
40+
CHART
41+
""
42+
"${oneValueArgs}"
43+
"${multiValueArgs}"
44+
${ARGN}
45+
)
46+
47+
string(REPLACE ".c" "" TARGET ${CHART_SOURCE})
48+
string(REPLACE "/" "." TARGET ${TARGET})
49+
set(CHART ${TARGET})
50+
string(PREPEND CHART "chart.")
51+
string(PREPEND TARGET "bench.")
52+
53+
set(BENCHMARK_CSV "${BENCHMARK_OUTPUT_DIR}/${TARGET}.csv")
54+
set(CHART_JSON "${CMAKE_SOURCE_DIR}/docs/_charts/bench.${CHART_NAME}.json")
55+
add_custom_command(
56+
OUTPUT ${CHART_JSON}
57+
DEPENDS ${BENCHMARK_CSV}
58+
COMMAND
59+
${Python_EXECUTABLE} ${BENCHMARK_CHART}
60+
${BENCHMARK_CSV} ${CHART_JSON} ${CHART_RUNS}
61+
)
62+
add_custom_target(${CHART} DEPENDS ${CHART_JSON})
63+
add_dependencies(charts ${CHART})
64+
endfunction()

docs/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ find_package(Sphinx REQUIRED)
33

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

78
set(DOXYGEN_INPUT_DIR ${PROJECT_SOURCE_DIR}/include)
89
set(DOXYGEN_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/doxygen)
@@ -37,8 +38,9 @@ add_custom_command(
3738
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
3839
DEPENDS
3940
${LEET_DOC_SOURCES}
41+
${LEET_CHART_SOURCES}
4042
${DOXYGEN_INDEX_FILE}
4143
MAIN_DEPENDENCY ${SPHINX_SOURCE}/conf.py
4244
COMMENT "Generating sphinx html."
4345
)
44-
add_custom_target(Sphinx ALL DEPENDS ${SPHINX_INDEX_FILE})
46+
add_custom_target(Sphinx ALL DEPENDS ${SPHINX_INDEX_FILE} charts)

docs/_charts/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# \_charts
2+
3+
`bench.*.json` files are generated from benchmarks.
4+
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.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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}}

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# -- General configuration ---------------------------------------------------
1414
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
1515

16-
extensions = ["breathe", "sphinx.ext.todo"]
16+
extensions = ["breathe", "sphinx.ext.todo", "sphinx_charts.charts"]
1717

1818
# Breathe configuration
1919
breathe_default_project = "leet"

docs/lib/ds/bstree.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
Binary search tree
22
==================
33

4+
Inserting data into a bstree
5+
----------------------------
6+
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.
7+
Writing your own insertion function is more performant since it avoids using a function pointer.
8+
9+
.. chart:: _charts/bench.bstree_insert_fnptr.json
10+
11+
1.000.000 random numbers inserted into a bstree (-O0, 100 runs)
12+
413
API
514
---
615

@@ -16,6 +25,7 @@ ______
1625
Functions
1726
_________
1827

28+
.. doxygenfunction:: bstree_link
1929
.. doxygenfunction:: bstree_insert
2030
.. doxygenfunction:: bstree_search
2131
.. doxygenfunction:: bstree_first

docs/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
breathe
22
sphinx-rtd-theme
3+
sphinx_charts

include/ds/bstree.h

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,24 @@ struct bstree
4343
*/
4444
void _transplant(struct bstree** root, struct bstree* u, struct bstree* v);
4545

46+
/**
47+
* @brief Grafts a new node into the tree at the given location.
48+
*
49+
* Utility for writing external insert functions. Writing your own insert
50+
* function is more performant since it avoids using a function pointer.
51+
*
52+
* @param link Location to insert the node.
53+
* @param n The node to insert.
54+
* @param parent Parent of the node to insert.
55+
*/
56+
void
57+
bstree_link(struct bstree** link, struct bstree* n, struct bstree* parent)
58+
{
59+
n->_parent = parent;
60+
n->_left = n->_right = NULL;
61+
*link = n;
62+
}
63+
4664
/**
4765
* @brief Inserts a node at the appropriate position on the tree.
4866
*
@@ -56,30 +74,27 @@ void _transplant(struct bstree** root, struct bstree* u, struct bstree* v);
5674
* @param n The node to insert.
5775
* @param cmp Insertion comparator.
5876
*/
59-
void
77+
bool
6078
bstree_insert(struct bstree** root, struct bstree* n, int (*cmp)(void*, void*))
6179
{
62-
struct bstree* x = *root;
63-
struct bstree* y = NULL;
64-
while (x)
80+
struct bstree** link = root;
81+
struct bstree* parent = NULL;
82+
while (*link)
6583
{
66-
// Invariant: x is not empty, the new node must be inserted to
67-
// the left or to the right.
68-
y = x;
69-
if (cmp(n, x) < 0)
70-
x = x->_left;
84+
// Invariant: link is not empty, the new node must be inserted
85+
// to the left or to the right.
86+
parent = *link;
87+
int result = cmp(n, *link);
88+
if (result < 0)
89+
link = &((*link)->_left);
90+
else if (result > 0)
91+
link = &((*link)->_right);
7192
else
72-
x = x->_right;
93+
return false;
7394
}
7495

75-
n->_parent = y;
76-
if (!y)
77-
// The tree was empty.
78-
*root = n;
79-
else if (cmp(n, y) < 0)
80-
y->_left = n;
81-
else
82-
y->_right = n;
96+
bstree_link(link, n, parent);
97+
return true;
8398
}
8499

85100
/**
@@ -143,7 +158,7 @@ bstree_last(struct bstree* n)
143158
*
144159
* Used to traverse the tree in order.
145160
*
146-
* `for (struct bstree* it = bstree_first(&root); it; it = bstree_next(it))`
161+
* `for (struct bstree* it = bstree_first(root); it; it = bstree_next(it))`
147162
*
148163
* @param n Handle to the current node.
149164
* @return Handle to the successor of the node.
@@ -169,7 +184,7 @@ bstree_next(struct bstree* n)
169184
*
170185
* Used to traverse the tree in reverse order.
171186
*
172-
* `for (struct bstree* it = bstree_last(&root); it; it = bstree_prev(it))`
187+
* `for (struct bstree* it = bstree_last(root); it; it = bstree_prev(it))`
173188
*
174189
* @param n Handle to the current node.
175190
* @return Handle to the predecessor of the node.

0 commit comments

Comments
 (0)