friggog / tree-gen

Procedural generation of tree models in blender
GNU General Public License v3.0
827 stars 74 forks source link

Improve performance and reduce memory footprint during generation of complex trees #46

Closed samipfjo closed 3 years ago

samipfjo commented 3 years ago

Since we don't dynamically add instance variables to objects during runtime, we can leverage __slots__ to remove the Leaf, Stem, and Tree classes' __dict__ attribute. This reduces memory requirements and increases performance on complex trees. Really doesn't make a difference for Tree, but I added anyway for consistency.

It doesn't make a difference in simple trees, but in complex trees like the Weeping Willow that have boatloads of branches and leaves it makes a significant difference. My tests show it shaves off about 11 seconds from generating a Weeping Willow while making no statistically significant difference for Black Oak.

I have apparently lost the memory benchmarks from these runs (I did them a while back), but since this removes the __dict__ attribute from each class it's not possible for the footprint to be worse.

W/O __slots__

Tree: Black Oak Seed: 8684998

Avg of 4 runs: create_branches: 3.99 seconds create_leaf_mesh: 7.77 seconds Tree generated in 11.75 seconds

Tree: Weeping Willow Seed: 8684998

Avg of 4 runs: create_branches: 76.21 seconds create_leaf_mesh: 18.00 seconds Tree generated in 93.86 seconds

====

__slots__

Tree: Black Oak Seed: 8684998

Avg of 4 runs: create_branches: 4.01 seconds create_leaf_mesh: 8.00 seconds Tree generated in 11.78 seconds

Tree: Weeping Willow Seed: 8684998

Avg of 4 runs: create_branches: 65.62 seconds create_leaf_mesh: 16.95 seconds Tree generated in 82.26 seconds

samipfjo commented 3 years ago

Squashed a fix for a dumb mistake

samipfjo commented 3 years ago

If you want to run your own tests, I used the decorator function below to run accurate benchmarks. The code is a modified version of the SO answer here.


import time
import os
import functools

def track(func):

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.process_time()
        result = func(*args, **kwargs)
        elapsed_time = time.process_time() - start

        print("\n{}: exec time: {}\n".format(
            func.__name__,
            elapsed_time))
        return result

    return wrapper
friggog commented 3 years ago

Nice! Do we need to bump the version for a new release?

samipfjo commented 3 years ago

I mean, it's a substantial improvement but ultimately a minor change. If we use the major.minor.patch format for versioning I guess it would count as a minor increment.

samipfjo commented 3 years ago

@friggog Reminder that this needs merging when you get a chance