-
Notifications
You must be signed in to change notification settings - Fork 16
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
Make num_bits a constant expression. #201
Merged
Merged
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The variable stores the number of bits per word and is used throughout the implementation to split a position into a word index and bit position in the word. The value can be determined statically from the size of the template parameter and does not need to be stored as a field. Keeping it as a `constexpr` value allows the compiler to optimize operations that use it. In particular, since the value is always a power of two, any multiplication, division, and modulo operations by this value, which are quite common, can be turned into bit shifts or masks by a constant. The performance impact of this change is quite small, but measurable, at around 2% for a 1M malariasimulation run, with no downside.
giovannic
approved these changes
Jul 16, 2024
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<3
plietar
added a commit
that referenced
this pull request
Sep 6, 2024
On multiple occasions, I have made changes to individual that inadvertently broke the ABI of the native code. Unfortunately, after the new version of individual was released and users installed it, the R tooling wasn't able to detect this situation and did not automatically force malariasimulation to be re-installed or re-compiled. As a result, users were using a malariasimulation shared library that was built against individual 0.X together with an individual shared library at version 0.(X+1). This causes very subtle and hard to diagnose bugs, where one piece of code might read from the wrong place. For example, #187 changed a return type of `NumericVariable::get_values` from a prvalue to a const-reference, avoiding unnecessary copies. For most intents and purposes, this is a API-compatible change, but the ABI of the method is different. After the update, if a user was still using a installation of malariasimulation that expected the old signature, the code would be compiled expecting to receive an `std::vector` by value (ie. three words representing the pointer to the data, the length and the capacity) instead received a pointer to such a vector. Another example is #201, which removed the `num_bits` field and made it a compile-time constant. Code which used `num_bits` was now reading from an unintialized piece of memory instead. While the `num_bits` field was private, header-file code from the individual package that reads the field can make its way into the malariasimulation shared library and embed the assumptions about the layout of the class. Given the heavy use of templates in the package, almost all of it is re-instantiated and included in the downstream shared library. In all these cases, recompiling malariasimulation is sufficient to ensure it uses the new ABI of the individual package. If the user uses `devtools`, then this simply requires them to call `devtools::clean_dll`. The issue is that identifying the problem is near impossible for a user. Nothing in the behaviour of malariasimulation points to an incompatibility, instead the package only misbehaves in very suprising ways. The goal of this change is to export the version of the individual package in its C++ headers. The constant may then be included and compiled into the malariasimulation shared object. At load-time, malariasimulation can compare this version with the result of `packageVersion("individual")` and show a warning or an error if they don't match. The user will be directed to re-install malariasimulation. Some alternatives I considered instead: 1. Don't make ABI breaking changes to individual: I think this is a non-starter. We do not want to restrict our abilities to improve and optimise the package. Moreover identifying what is or isn't a breaking change is quite challenging. 2. Stop exposing a C++ API, and require users to call the R functions instead: allowing users to write high-performant processes or utility functions for the hot path of their models is quite an important aspect of individual. 3. Break the ABI but announce it in release notes so users know to be on the look out and to re-compile their copy of malariasimulation: no one reads release notes, especially for a package they only use indirectly. Moreover it still requires identifying ABI breaking changes. 4. Implement the suggested scheme, but use an ABI version instead of the package version. This will avoid false positives, where the package's version number is increased even though the ABI hasn't changed. I think the rate of releases and the cost of recompiling malariasimulation are low enough that it is better to err on the safe side and treat every release as if it could be ABI breaking.
plietar
added a commit
that referenced
this pull request
Sep 6, 2024
On multiple occasions, I have made changes to individual that inadvertently broke the ABI of the native code. Unfortunately, after the new version of individual was released and users installed it, the R tooling wasn't able to detect this situation and did not automatically force malariasimulation to be re-installed or re-compiled. As a result, users were using a malariasimulation shared library that was built against individual 0.X together with an individual shared library at version 0.(X+1). This causes very subtle and hard to diagnose bugs, where one piece of code might read from the wrong place. For example, #187 changed a return type of `NumericVariable::get_values` from a prvalue to a const-reference, avoiding unnecessary copies. For most intents and purposes, this is a API-compatible change, but the ABI of the method is different. After the update, if a user was still using a installation of malariasimulation that expected the old signature, the code would be compiled expecting to receive an `std::vector` by value (ie. three words representing the pointer to the data, the length and the capacity) instead received a pointer to such a vector. Another example is #201, which removed the `num_bits` field and made it a compile-time constant. Code which used `num_bits` was now reading from an unintialized piece of memory instead. While the `num_bits` field was private, header-file code from the individual package that reads the field can make its way into the malariasimulation shared library and embed the assumptions about the layout of the class. Given the heavy use of templates in the package, almost all of it is re-instantiated and included in the downstream shared library. In all these cases, recompiling malariasimulation is sufficient to ensure it uses the new ABI of the individual package. If the user uses `devtools`, then this simply requires them to call `devtools::clean_dll`. The issue is that identifying the problem is near impossible for a user. Nothing in the behaviour of malariasimulation points to an incompatibility, instead the package only misbehaves in very suprising ways. The goal of this change is to export the version of the individual package in its C++ headers. The constant may then be included and compiled into the malariasimulation shared object. At load-time, malariasimulation can compare this version with the result of `packageVersion("individual")` and show a warning or an error if they don't match. The user will be directed to re-install malariasimulation. I could not figure out a way of exporting the version from `DESCRIPTION` into header files. It's possibly something `Rcpp::compileAttributes()` could do, but it doesn't. Instead the version number is duplicated and a CI check ensures that they are kept in sync. Some alternatives I considered instead: 1. Don't make ABI breaking changes to individual: I think this is a non-starter. We do not want to restrict our abilities to improve and optimise the package. Moreover identifying what is or isn't a breaking change is quite challenging. 2. Stop exposing a C++ API, and require users to call the R functions instead: allowing users to write high-performant processes or utility functions for the hot path of their models is quite an important aspect of individual. 3. Break the ABI but announce it in release notes so users know to be on the look out and to re-compile their copy of malariasimulation: no one reads release notes, especially for a package they only use indirectly. Moreover it still requires identifying ABI breaking changes. 4. Implement the suggested scheme, but use an ABI version instead of the package version. This will avoid false positives, where the package's version number is increased even though the ABI hasn't changed. I think the rate of releases and the cost of recompiling malariasimulation are low enough that it is better to err on the safe side and treat every release as if it could be ABI breaking.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
The variable stores the number of bits per word and is used throughout the implementation to split a position into a word index and bit position in the word.
The value can be determined statically from the size of the template parameter and does not need to be stored as a field. Keeping it as a
constexpr
value allows the compiler to optimize operations that use it. In particular, since the value is always a power of two, any multiplication, division, and modulo operations by this value, which are quite common, can be turned into bit shifts or masks by a constant.The performance impact of this change is quite small, but measurable, at around 2% for a 1M malariasimulation run, with no downside.