Skip to content

Commit

Permalink
fix!: improve performances
Browse files Browse the repository at this point in the history
  • Loading branch information
jeanchristopheruel committed Mar 17, 2024
1 parent d736128 commit 534c765
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 281 deletions.
77 changes: 13 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ A simple yet fast header-only library to read/write, serialize/deserialize STL (
[![Python](https://img.shields.io/pypi/pyversions/openstl.svg)](https://pypi.org/project/openstl/)

# Performances benchmark
The benchmark results indicate that the performance of multiplying OpenSTL triangles is comparable to that of
native NumPy array multiplication.
Discover the staggering performance of OpenSTL in comparison to numpy-stl, thanks to its powerful C++ backend.

Write: 2 to 3 times faster than numpy-stl.
Read: 1 to 12 times faster than numpy-stl.
Rotate: 1 to 12 times faster than numpy-stl.

![Benchmark Results](benchmark/benchmark.png)

# Python Usage
Expand All @@ -21,17 +25,15 @@ native NumPy array multiplication.
```python
import openstl

# Define a list of triangles
# Following the STL standard, each triangle is defined with : normal, v0, v1, v2, attribute_byte_count
# Note: attribute_byte_count can be used for coloring
triangles = [
openstl.Triangle([0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0], 3),
openstl.Triangle([0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0], 3),
openstl.Triangle([0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0], 3)
]
# Define an array of triangles
# Following the STL standard, each triangle is defined with : normal, v0, v1, v2
triangles = np.array([
[[0.0, 0.0, 1.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
[[0.0, 0.0, 1.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0]],
])

# Serialize the triangles to a file
openstl.write("output.stl", triangles, openstl.StlFormat.Binary)
openstl.write("output.stl", triangles, openstl.format.binary) # Or openstl.format.ascii (slower but human readable)

# Deserialize triangles from a file
deserialized_triangles = openstl.read("output.stl")
Expand All @@ -40,59 +42,6 @@ deserialized_triangles = openstl.read("output.stl")
print("Deserialized Triangles:", deserialized_triangles)
```

Notes:
- The format `openstl.StlFormat.ASCII` can be used to make the file human-readable.
It is however slower than Binary for reading and writing.
- `attribute_byte_count` can only be written in **Binary** format.
The STL standard do not include it in the ASCII format.

### Access the Triangle attributes
```python
import openstl

# Following the STL standard, each triangle is defined with : normal, v0, v1, v2, attribute_byte_count
triangle = openstl.Triangle([0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0], 3)

# Access individual vertices
print("Normal:", triangle.normal)
print("Vertex 0:", triangle.v0)
print("Vertex 1:", triangle.v1)
print("Vertex 2:", triangle.v2)

# Access normal and attribute_byte_count
print("Attribute Byte Count:", triangle.attribute_byte_count)

# Get length of Triangle (number of elements in buffer)
print("Length:", len(triangle))

# Get elements using indexing (inefficient, instead use get_vertices)
print("Elements:", [triangle[i] for i in range(len(triangle))])

# Print Triangle object
print("Triangle:", triangle)
```

### Get an efficient view of the vertices
```python
import openstl

# Define a list of triangles
triangles = [
openstl.Triangle([0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0], 3),
openstl.Triangle([0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0], 3),
openstl.Triangle([0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0], 3)
]

# Get a view of the vertices (v0, v1, v2) of all triangles by skipping normals and attribute_byte_count
vertices = openstl.get_vertices(triangles)

# Print the shape of the array
print("Shape of vertices array:", vertices.shape)

# Print the vertices array
print("Vertices:", vertices)
```

# C++ Usage
### Read STL from file
```c++
Expand Down
Binary file modified benchmark/benchmark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 63 additions & 29 deletions benchmark/benchtest.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,111 @@
import openstl
import numpy as np
import timeit
from stl import mesh, Mode
import matplotlib.pyplot as plt

def create_triangles(num_triangles):
triangles = []
tri = openstl.Triangle([0, 0, 1], [1, 1, 1], [2, 2, 2], [3, 3, 3], 3)
tri = np.array([[0, 0, 1], [1, 1, 1], [2, 2, 2], [3, 3, 3]]) # normal, vertice 1,2,3
for _ in range(num_triangles):
triangles.append(tri)
return triangles
return np.array(triangles)

def benchmark_write(num_triangles, filename):
triangles = create_triangles(num_triangles)
result = timeit.timeit(lambda: openstl.write(filename, triangles, openstl.StlFormat.Binary), number=5)
result = timeit.timeit(lambda: openstl.write(filename, triangles, openstl.format.binary), number=5)
return result

def benchmark_read(filename):
result = timeit.timeit(lambda: openstl.read(filename), number=5)
return result

def benchmark_multiply_triangles(num_triangles, matrix):
def benchmark_rotate(num_triangles):
triangles = create_triangles(num_triangles)
vertices = openstl.get_vertices(triangles).reshape(3,-1)
result = timeit.timeit(lambda: np.matmul(matrix, vertices), number=5)
cos, sin = np.cos(np.pi/3), np.sin(np.pi/3)
matrix = np.array([[cos, -sin, 0],[sin, cos, 0], [0,0,1]]) # rotation of pi/3 around z axis
result = timeit.timeit(lambda: np.matmul(matrix, triangles.reshape(-1,3).T), number=5)
return result

def benchmark_multiply_numpy(num_triangles, matrix):
array = np.random.rand(3, num_triangles*3) # 3 float per triangle
result = timeit.timeit(lambda: np.matmul(matrix, array), number=5)
def create_nympystl(num_triangles):
triangles = np.zeros(num_triangles, dtype=mesh.Mesh.dtype)
mesh_data = mesh.Mesh(triangles)
return mesh_data

def benchmark_read_numpy_stl(filename):
result = timeit.timeit(lambda: mesh.Mesh.from_file(filename, mode=Mode.BINARY), number=5)
return result

def benchmark_write_numpy_stl(num_triangles, filename):
mesh_data = create_nympystl(num_triangles)
result = timeit.timeit(lambda: mesh_data.save(filename), number=5)
return result

def benchmark_rotate_numpy(num_triangles):
mesh_data = create_nympystl(num_triangles)
result = timeit.timeit(lambda: mesh_data.rotate([0.0, 0.0, 1.0], np.pi/3), number=5)
return result

if __name__ == "__main__":
# Benchmark parameters
filename = "../tests/python/benchmark.stl"
matrix = np.random.rand(3, 3)
num_triangles_list = [10**i for i in range(1, 7)]
filename = "benchmark.stl"
num_triangles_list = np.logspace(1,7, 20).round().astype(int)

# Benchmarking
write_times = []
read_times = []
multiply_times = []
multiply_numpy_times = []
write_times_openstl = []
read_times_openstl = []
rotate_openstl = []
read_numpy_stl_times = []
write_numpy_stl_times = []
rotate_numpy_stl = []

# Exclude 1rst operation (openstl dyn lib loading)
write_time = benchmark_write(1, filename)

for num_triangles in num_triangles_list:
write_time = benchmark_write(num_triangles, filename)
read_time = benchmark_read(filename)
multiply_time = benchmark_multiply_triangles(num_triangles, matrix)
multiply_numpy_time = benchmark_multiply_numpy(num_triangles, matrix)
rotate_time = benchmark_rotate(num_triangles)
write_times_openstl.append(write_time)
read_times_openstl.append(read_time)
rotate_openstl.append(rotate_time)

write_time = benchmark_write_numpy_stl(num_triangles, filename)
read_time = benchmark_read_numpy_stl(filename)
rotate_time = benchmark_rotate_numpy(num_triangles)
read_numpy_stl_times.append(read_time)
write_numpy_stl_times.append(write_time)
rotate_numpy_stl.append(rotate_time)

write_times.append(write_time)
read_times.append(read_time)
multiply_times.append(multiply_time)
multiply_numpy_times.append(multiply_numpy_time)
# Results
write_diff = (np.array(write_numpy_stl_times) / np.array(write_times_openstl)).round(3)
read_diff = (np.array(read_numpy_stl_times) / np.array(read_times_openstl)).round(3)
rotate_diff = (np.array(rotate_numpy_stl) / np.array(rotate_openstl)).round(3)
print(f"Write:\tOpenSTL is { np.min(write_diff)} to {np.max(write_diff)} X faster than numpy-stl")
print(f"Read:\tOpenSTL is {np.min(read_diff)} to {np.max(read_diff)} X faster than numpy-stl")
print(f"Rotate:\tOpenSTL is {np.min(rotate_diff)} to {np.max(rotate_diff)} X faster than numpy-stl")

# Plotting
plt.figure(figsize=(10, 6)) # Set figure size for better visualization
plt.plot(num_triangles_list, write_times, label="Write", color="green", linestyle="-", marker="o", markersize=5)
plt.plot(num_triangles_list, read_times, label="Read", color="blue", linestyle="--", marker="s", markersize=5)
plt.plot(num_triangles_list, multiply_times, label="Multiply (Triangles)", color="red", linestyle="-.", marker="^", markersize=5)
plt.plot(num_triangles_list, multiply_numpy_times, label="Multiply (Numpy Array)", color="purple", linestyle=":", marker="d", markersize=5)
plt.plot(num_triangles_list, write_times_openstl, label="Write (OpenSTL)", color="green", linestyle="-", marker="s", markersize=5)
plt.plot(num_triangles_list, read_times_openstl, label="Read (OpenSTL)", color="blue", linestyle="-", marker="s", markersize=5)
plt.plot(num_triangles_list, rotate_openstl, label="Rotate (OpenSTL)", color="red", linestyle="-", marker="s", markersize=5)
plt.plot(num_triangles_list, write_numpy_stl_times, label="Write (numpy-stl)", color="green", linestyle="--", marker="o", markersize=5)
plt.plot(num_triangles_list, read_numpy_stl_times, label="Read (numpy-stl)", color="blue", linestyle="--", marker="o", markersize=5)
plt.plot(num_triangles_list, rotate_numpy_stl, label="Rotate (numpy-stl)", color="red", linestyle="--", marker="o", markersize=5)

plt.xlabel("Number of Triangles", fontsize=12)
plt.ylabel("Time (seconds)", fontsize=12)
plt.title("Python Benchmark Results", fontsize=14)
plt.xscale("log")
plt.yscale("log")
plt.legend(fontsize=10)
plt.legend(fontsize=10, handlelength=5)
plt.grid(True, which="both", linestyle="--", linewidth=0.5) # Add grid lines with dashed style
plt.tight_layout() # Adjust layout to prevent clipping of labels

# Adjusting axes limits and scaling
plt.xlim(num_triangles_list[0], num_triangles_list[-1]*1.1) # Extend x-axis range by 10%
plt.ylim(min(min(write_times), min(read_times), min(multiply_times), min(multiply_numpy_times)) * 0.5,
max(max(write_times), max(read_times), max(multiply_times), max(multiply_numpy_times)) * 2) # Expand y-axis range
plt.ylim(min(min(write_times_openstl), min(read_times_openstl), min(read_numpy_stl_times), min(write_numpy_stl_times)) * 0.5,
max(max(write_times_openstl), max(read_times_openstl), max(read_numpy_stl_times), max(write_numpy_stl_times)) * 2) # Expand y-axis range

plt.show()
11 changes: 2 additions & 9 deletions cmake/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,7 @@ endmacro()

macro(ReadDependencyVersion dependency_name version_file)
file(READ ${version_file} ver)
string(REGEX MATCH "${dependency_name}\ *=\ *\"([0-9]+).([0-9]+).?([0-9]*)\"" _ ${ver})
set(${dependency_name}_VERSION_MAJOR ${CMAKE_MATCH_1})
set(${dependency_name}_VERSION_MINOR ${CMAKE_MATCH_2})
set(${dependency_name}_VERSION_PATCH ${CMAKE_MATCH_3})
if(CMAKE_MATCH_3 EQUAL "")
set(${dependency_name}_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}")
else()
set(${dependency_name}_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
endif()
string(REGEX MATCH "${dependency_name}\ *=\ *\"([^\"]*)\"" _ ${ver})
set(${dependency_name}_VERSION "${CMAKE_MATCH_1}")
message(STATUS "${dependency_name} version: ${${dependency_name}_VERSION}")
endmacro()
1 change: 1 addition & 0 deletions modules/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ configure_file(include/openstl/core/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/gen
#-------------------------------------------------------------------------------
add_library(openstl_core INTERFACE)
target_include_directories(openstl_core INTERFACE include/ ${CMAKE_CURRENT_BINARY_DIR}/generated/)
target_link_libraries(openstl_core INTERFACE)
add_library(openstl::core ALIAS openstl_core)
25 changes: 13 additions & 12 deletions modules/core/include/openstl/core/stl.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ namespace openstl
* @param triangles The vector of triangles to serialize.
* @param stream The output stream to write the serialized data to.
*/
template<typename Stream>
void serializeAsciiStl(const std::vector<Triangle>& triangles, Stream& stream) {
template<typename Stream, typename Container>
void serializeAsciiStl(const Container& triangles, Stream& stream) {
stream << "solid\n";
for (const auto& triangle : triangles) {
stream << "facet normal " << triangle.normal.x << " " << triangle.normal.y << " " << triangle.normal.z << std::endl;
for (const auto& tri : triangles) {
stream << "facet normal " << tri.normal.x << " " << tri.normal.y << " " << tri.normal.z << std::endl;
stream << "outer loop" << std::endl;
stream << "vertex " << triangle.v0.x << " " << triangle.v0.y << " " << triangle.v0.z << std::endl;
stream << "vertex " << triangle.v1.x << " " << triangle.v1.y << " " << triangle.v1.z << std::endl;
stream << "vertex " << triangle.v2.x << " " << triangle.v2.y << " " << triangle.v2.z << std::endl;
stream << "vertex " << tri.v0.x << " " << tri.v0.y << " " << tri.v0.z << std::endl;
stream << "vertex " << tri.v1.x << " " << tri.v1.y << " " << tri.v1.z << std::endl;
stream << "vertex " << tri.v2.x << " " << tri.v2.y << " " << tri.v2.z << std::endl;
stream << "endloop" << std::endl;
stream << "endfacet" << std::endl;
}
Expand All @@ -88,8 +88,8 @@ namespace openstl
* @param triangles The vector of triangles to serialize.
* @param stream The output stream to write the serialized data.
*/
template<typename Stream>
void serializeBinaryStl(const std::vector<Triangle>& triangles, Stream& stream) {
template<typename Stream, typename Container>
void serializeBinaryStl(const Container& triangles, Stream& stream) {
// Write header (80 bytes for comments)
char header[80] = {0};
stream.write(header, 80);
Expand All @@ -99,7 +99,8 @@ namespace openstl
stream.write((const char*)&triangleCount, sizeof(triangleCount));

// Write triangles
stream.write((const char*)triangles.data(), sizeof(Triangle)*triangles.size());
for (const auto& tri : triangles)
stream.write((const char*)&tri, sizeof(Triangle));
}

/**
Expand All @@ -110,8 +111,8 @@ namespace openstl
* @param stream The output stream to write the serialized data.
* @param format The format of the STL file (ASCII or binary).
*/
template <typename Stream>
inline void serializeStl(const std::vector<Triangle>& triangles, Stream& stream, StlFormat format) {
template <typename Stream, typename Container>
inline void serializeStl(const Container& triangles, Stream& stream, StlFormat format) {
switch (format) {
case StlFormat::ASCII:
serializeAsciiStl(triangles, stream);
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ tag_format = "v$version"

[tool.poetry.dependencies]
pybind11 = "2.11.1"
catch2 = "3.4.0"
catch2 = "3.5.3"
2 changes: 1 addition & 1 deletion python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ IF(PYTHONINTERP_FOUND AND PYTHONLIBS_FOUND)
file(GLOB_RECURSE python_SRC core/*.cpp)
pybind11_add_module(openstl MODULE ${python_SRC})
target_include_directories(openstl PRIVATE ${PYBIND11_SUBMODULE}/include)
target_link_libraries(openstl PRIVATE openstl::core pybind11::headers)
target_link_libraries(openstl PRIVATE openstl::core pybind11::headers)
target_compile_definitions(openstl PRIVATE VERSION_INFO=${PROJECT_VERSION})
set_target_properties(openstl PROPERTIES
INTERPROCEDURAL_OPTIMIZATION ON
Expand Down
Loading

0 comments on commit 534c765

Please sign in to comment.