Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement managed raster class in C++ #1597

Open
wants to merge 78 commits into
base: feature/routing-refactor
Choose a base branch
from

Conversation

emlys
Copy link
Member

@emlys emlys commented Jul 17, 2024

Description

This PR reimplements the ManagedRaster class in C++ for better performance. Tested with sdr.calculate_sediment_deposition and the SDR sample data, it's now just slightly faster than the original implementation on main (0.59 vs 0.61 seconds), whereas the python implementation on feature/routing-refactor takes nearly twice as long.

I'm a beginner at C++ so I'm sure I'm not always using the right conventions or design patterns, but this seems like an okay starting point. I took some guidance from the Google C++ style guide. Ignoring some guidelines that don't seem to make sense in this context. For instance, I have all the code in a header file because splitting it out into ManagedRaster.h and ManagedRaster.cpp significantly slowed it down.

Happy to discuss and walk through this PR on a zoom call as well.

Checklist

  • Updated HISTORY.rst and link to any relevant issue (if these changes are user-facing)
  • Updated the user's guide (if needed)
  • Tested the Workbench UI (if relevant)

@emlys emlys changed the base branch from main to feature/routing-refactor July 17, 2024 18:00
Copy link
Member

@phargogh phargogh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really great @emlys ! I had a few questions, but I think they're all pretty minor. Happy to talk through anything that would be helpful!

setup.py Outdated
Comment on lines 24 to 31
if platform.system() == 'Darwin':
compiler_and_linker_args = ['-stdlib=libc++']
compiler_args = []
compiler_and_linker_args = ['-stdlib=libc++', '-std=c++20']
library_dirs = [f'{os.environ["CONDA_PREFIX"]}/lib']
else:
compiler_args = ['/std:c++20']
library_dirs = [f'{os.environ["CONDA_PREFIX"]}/Library/lib']
include_dirs.append(f'{os.environ["CONDA_PREFIX"]}/Library/include')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's possible that someone would compile on Linux, would the /srd:c++20 flag for with GNU? Or do we need to handle the linux possibility separately?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we been supporting Linux compilation previously? I think this would not work, but if it didn't work before, maybe that's a separate question.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's never been a priority in this repository, but we do offer linux builds via conda-forge so I don't think we should completely ignore the use case. Linux support has historically been kind of informal in this repo.

raster_x_size = dataset->GetRasterXSize();
raster_y_size = dataset->GetRasterYSize();

if (band_id < 1 or band_id > dataset->GetRasterCount()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fascinating, I wasn't aware that or was a keyword here in C++! Looks like their alternative keywords can be pretty cryptic, but this one is quite readable.

closed = 0;
}

void set(long xi, long yi, double value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I remember Rich using the inline keyword here as well to try and eke out some minor speedup since the function was so small. Is there any noticeable speedup here by using void inline set(...? There might not be, and the function is long enough where there probably isn't a noticeable speedup.

Copy link
Member Author

@emlys emlys Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing a measurable speedup on the SDR sample data from inlining this (or the other function below). As a guideline, the Google C++ style guide recommends against inlining functions over about 10 lines, so these are on the high end of that. But since they were originally inlined in the Cython version, I don't see any harm in making them inlined here!

-> Caveat that I'm not at all confident that my speed testing will detect relatively small changes. I get a lot of variation in times.

src/natcap/invest/managed_raster/ManagedRaster.h Outdated Show resolved Hide resolved
}
}

double get(long xi, long yi) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here about whether there's any speedup from adding the inline keyword.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above

Comment on lines +76 to +77
cdef ManagedRaster stream_raster = ManagedRaster(
stream_path.encode('utf-8'), 1, False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine once we have the python C interface for logging figured out that we might be able to do this string encoding as well!

Comment on lines 242 to 243
neighbor = up_iterator.next()
while neighbor.direction < 8:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember one of the things we talked about last week was the need to be careful to cover all of the iteration cases (e.g. continue, end of loop block) with calling up_iterator.next(). What if we just handled that up here at the top of the loop? I suppose there's a slight cost to holding 2 neighbor objects in memory, but maybe it would be easier to maintain? I'm thinking of something like the below (github wouldn't allow me to suggest something on the whole loop block)

Suggested change
neighbor = up_iterator.next()
while neighbor.direction < 8:
neighbor = up_iterator.next()
next_neighbor = neighbor
while neighbor.direction < 8:
neighbor = next_neighbor
next_neighbor = up_iterator.next()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@phargogh I think I've found an even better solution which is to properly implement the iterators to meet the C++ iterator specification. That way we don't need to manually call next() at all. I was stumped on that before but I think I've worked it out.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woohoo! That is super exciting. Well done and I can't wait to see the iterator!

up_iterator = UpslopeNeighborIterator(
mfd_flow_direction_raster, global_col, global_row)
neighbor = up_iterator.next()
while neighbor.direction < 8:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here about whether it'd make sense to track a neighbor/next neighbor object in the loop to make it easier on us to maintain and avoid infinite loops.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember we talked about this file, but I forget if it's intended to continue to be in the PR. Should this file be removed as a part of the PR or should it be retained for now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, it's meant to be removed!

Comment on lines 119 to 120
for i, et0_path in enumerate(et0_path_list):
et0_m_rasters.push_back(ManagedRaster(et0_path.encode('utf-8'), 1, 0))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the enumeration still needed? It looks like there are a few more enumerations in the next 30 lines or so

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants