-
Notifications
You must be signed in to change notification settings - Fork 246
Add C++ DynamicArrays
in Brian2 for runtime mode
#1650
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
Open
Legend101Zz
wants to merge
96
commits into
brian-team:master
Choose a base branch
from
Legend101Zz:feat/cppDynamicArray
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,367
−431
Open
Changes from 5 commits
Commits
Show all changes
96 commits
Select commit
Hold shift + click to select a range
a72d978
feat: add C++ DynamicArrays in Brian2 for runtime mode
Legend101Zz e8eaaca
feat: add DynamicArray1D and refactor DynamicArray2D to store data in…
Legend101Zz b8b9903
fix: betetr format
Legend101Zz b38edff
feat: add cython wrapper for dynamic array
Legend101Zz b35f1ba
Merge remote-tracking branch 'origin' into feat/cppDynamicArray
Legend101Zz 2f8beba
fix: cythondynamic error bugs and refactor setup file to build cython…
Legend101Zz 06986e4
refactor python implementation of dynamic array to just export cython…
Legend101Zz ed5460e
Fix testsuite failure due to using source instead of installed module
mstimberg 4765ae3
Delete brian2/memory/cdynamicarray.h
Legend101Zz fdda525
fix: typo init method
Legend101Zz e8dc0f7
fix: use in32_t,in64_t instead of int and long primitives
Legend101Zz e2dd337
fix: synapses test
Legend101Zz 2067307
refactor: cpp dynamic array (Clang on macOS 13) is running in C++03 …
Legend101Zz 8b95ca6
refactor(cppDynamicArray) : remove noexcept specifier
Legend101Zz e0da17e
fix: add resize_along_first method
Legend101Zz affd36e
feat: add resize_along_first method to cpp dynamic array implementation
Legend101Zz d8947d5
fix: use actual resize_along_first method instead of calling resize
Legend101Zz 4844ca4
fix: remove support for higher dim arrays
Legend101Zz 1d1563f
feat: add pycapsule method to dynamic array to share class pointers
Legend101Zz 1a71750
refactor(get_array_name): change get_array_name and DynamicVariables …
Legend101Zz 01f96f1
fix(minor): remove logs
Legend101Zz 6fda095
fix(minor): remove logs
Legend101Zz 412d6e6
revert: `get_pointer`param added to `get_array_name`
Legend101Zz 680ec5f
refactor: determine_keywords to make them generate code to handle dyn…
Legend101Zz 3c822c2
refactor(variables_to_namespace): Adding logic to pass in the capsule…
Legend101Zz 63f7d0c
refactor: cython extension manager to build from cpp dynamic array
Legend101Zz e819134
refactor: minor addition in devices to access dynamic array name in t…
Legend101Zz 803b464
refactor: common_group.pyx template to include dynamic cpp array decl…
Legend101Zz 58994f0
refactor: ratemonitor.pyx template
Legend101Zz 688a6c5
refactor: statemonitor.pyx template
Legend101Zz 1330d8d
refactor: synapses_create_array.pyx template
Legend101Zz d6b89c8
refactor: synapses_create_generator.pyx template
Legend101Zz 2572b0f
fix: ratemoniter hardcoded types
Legend101Zz 4579318
Test string expression get/set with Cython
mstimberg d83b5e7
Fix test issues revealed by previous commit
mstimberg 5971c7c
Chain test commands to fail if tests fail
mstimberg 7e1bab7
Avoid using Python version >=3.11.9 but <3.12 for tests
mstimberg 33907e9
fix(determine_keywords): to handle C++ pointer to the raw data buffer…
Legend101Zz c6dc665
fix: synapses templates
Legend101Zz a5d93aa
fix: cython dynamic array wrapper to handle boolean arrays as char
Legend101Zz 079f51c
fix(variables): doc string
Legend101Zz daa5ee6
fix:change "cython.bint" to "char"
Legend101Zz 7e7aebd
fix: add shrink method to dynamic arrays
Legend101Zz 31416ab
fix: spikemoniter template to use direct cpp pointers
Legend101Zz b18ca43
fix: ratemoniter template to use direct cpp dynamic array pointers
Legend101Zz 9f3007c
fix: dynamic array implementation to fix standalone test failures
Legend101Zz 4f6a016
fix: stride logic Memory has gaps case
Legend101Zz 7c39eb6
fix: comments
Legend101Zz ecc73f5
fix: get_dynamic_array_cpp_type function to return char for bool type
Legend101Zz 92a541c
fix: determine_keywords method to add the unique dynamic array name t…
Legend101Zz 58d9d22
fix(test): test_state_variables_group_as_index_problematic now explic…
Legend101Zz 0d492b2
fix:typo
Legend101Zz 395df27
fix(cythondynamicarray): data attribute for 2d array fixed
Legend101Zz 1ecea4e
fix: test failures
Legend101Zz 556f217
fix: confest file
Legend101Zz e442011
fix: syntax error in cython file
Legend101Zz 7e24eb4
revert:To "fix(cythondynamicarray): data attribute for 2d array fixed"
Legend101Zz 486a147
revert: changes done to fix test failures
Legend101Zz 78a8d7a
revert: confest and cython dynamic array changes
Legend101Zz d65aae1
fix: issue of creating a 0 sized 2d array
Legend101Zz 10e44c1
fix: dynamic array 2d data access attribute again
Legend101Zz c96de4a
fix: add explicit zero-initialization in dynamic arrays + test Clean …
Legend101Zz 4655049
test: change testsuite to Run Only Failing Test
Legend101Zz 9885b90
test: change testsuite to Run Only Failing Test -2
Legend101Zz 2003ddf
test: redo changes
Legend101Zz 1b602ec
fix: dynamic array failing test
Legend101Zz 7ec19b1
fix:the garbage values in dynamic array test
Legend101Zz 8b9bcef
recommit : testsuite workflow
Legend101Zz 77c5df7
fix:testsuite
Legend101Zz 1a3b2d9
fix: test_openmp_consistency
Legend101Zz e4cdc2c
fix: testsuite ci
Legend101Zz 6a3b6c7
redo: testsuite ci changes
Legend101Zz abca2dd
fix: redo test changes
Legend101Zz b4a25e3
fix: non-deterministic behavior of test_openmp_consistency
Legend101Zz aa60833
fix: broken test_openmp_consistency test
Legend101Zz 318f998
fix: broken test_openmp_consistency test-2
Legend101Zz c23a5d7
fix: broken test_openmp_consistency test-3
Legend101Zz 42a7c3f
cleanup(testsuite) : remove debug logs
Legend101Zz b3968ae
Merge branch 'master' into feat/cppDynamicArray
mstimberg e16ac73
Fix syntax error in yaml file after conflict resolution
mstimberg b8477f7
Workaround for size update of SpikeMonitor variables
mstimberg 5503588
cleanup(testsuite)-2: remove added Reset any preference changes code …
Legend101Zz f1ef25f
cleanup(testsuite)-2-retry: remove added Reset any preference changes…
Legend101Zz 732cbc5
cleanup(testsuite)-3: remove added copy fn for variable view as it al…
Legend101Zz 461b8a2
Revert "cleanup(testsuite)-3: remove added copy fn for variable view …
Legend101Zz 155f15d
review-changes: CythonCodeGenerator
Legend101Zz dab1685
review-changes: spikemoniter template
Legend101Zz ec31973
review-changes: class DynamicArray cpp
Legend101Zz 2b338a3
fix: typo in setup file
Legend101Zz cc06938
review-change: remove comment
Legend101Zz 4eac187
refactor: resize 2d dynamicArray to only resize along rows ( 1st dim)
Legend101Zz 2d22b87
fix: test_memory
Legend101Zz d985c15
refactor: statemoniter template to use direct cpp resize methods inst…
Legend101Zz ed3b3dc
fix: Workaround for size update of StateMonitor variables
Legend101Zz 648ce77
fix: Update the time variable also ( as it is not in record_variables )
Legend101Zz 9dec50d
fix: handle 1D and 2D arrays sizes correctly for the after run
Legend101Zz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
373 changes: 299 additions & 74 deletions
373
brian2/devices/cpp_standalone/brianlib/dynamic_array.h
This file contains hidden or 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,87 +1,312 @@ | ||
#ifndef _BRIAN_DYNAMIC_ARRAY_H | ||
#define _BRIAN_DYNAMIC_ARRAY_H | ||
|
||
#include<vector> | ||
#include <vector> | ||
#include <algorithm> | ||
#include <cstring> | ||
#include <cassert> | ||
|
||
/* | ||
* 2D Dynamic array class | ||
/** | ||
* A simple 1D dynamic array that grows efficiently over time. | ||
* | ||
* Efficiency note: if you are regularly resizing, make sure it is the first dimension that | ||
* is resized, not the second one. | ||
* This class is designed to mimic the behavior of C-style contiguous memory, | ||
* making it suitable for interop with tools like Cython and NumPy. | ||
* | ||
* Internally, it keeps track of: | ||
* - `m_size`: the number of elements the user is actively working with. | ||
* - `m_data.capacity()`: the total number of elements currently allocated. | ||
* | ||
* When growing, it over-allocates using a growth factor to avoid frequent | ||
* memory reallocations — giving us amortized O(1) behavior for appending elements. | ||
* | ||
* When shrinking, it simply zeroes out the unused portion instead of | ||
* releasing memory immediately. To actually free that extra memory, | ||
* call `shrink_to_fit()`. | ||
*/ | ||
template<class T> | ||
template <class T> | ||
class DynamicArray1D | ||
{ | ||
private: | ||
std::vector<T> m_data; | ||
size_t m_size; // Logical size (what user sees) | ||
double m_growth_factor; | ||
|
||
public: | ||
/** | ||
* | ||
* We call m_data.resize(initial_size) to ensure that operator[] is safe up to | ||
* initial_size-1 immediately after construction. This also sets capacity() to | ||
* at least initial_size. | ||
*/ | ||
DynamicArray1D(size_t initial_size = 0, double factor = 2.0) | ||
Legend101Zz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
: m_size(initial_size), m_growth_factor(factor) | ||
{ | ||
m_data.resize(initial_size); | ||
} | ||
|
||
~DynamicArray1D() = default; // note earlier we needed a destructor properly because we had a vector of pointers ... | ||
|
||
/** | ||
* @brief Resizes the array to a new logical size. | ||
* | ||
* If the new size is larger than the current capacity, we grow the buffer. | ||
* To avoid frequent reallocations, we over-allocate using a growth factor— | ||
* that means the actual buffer might grow more than you asked for. | ||
* This helps keep future resizes fast (amortized O(1) behavior). | ||
* | ||
* If the new size is smaller than the current logical size, we don't shrink | ||
* the buffer immediately. Instead, we zero out the unused part to avoid | ||
* keeping stale data around. If you really want to release unused memory, | ||
* call `shrink_to_fit()` separately. | ||
*/ | ||
void resize(size_t new_size) | ||
{ | ||
if (new_size > m_data.size()) | ||
{ | ||
// Growing: allocate more than strictly needed to reduce future allocations | ||
size_t grown = static_cast<size_t>(m_data.size() * m_growth_factor) + 1; | ||
size_t new_capacity = std::max(new_size, grown); | ||
m_data.resize(new_capacity); | ||
} | ||
else if (new_size < m_size) | ||
{ | ||
// Shrinking: zero out "deleted" entries for safety | ||
std::fill(m_data.begin() + new_size, | ||
m_data.begin() + m_size, | ||
T(0)); | ||
} | ||
m_size = new_size; | ||
} | ||
|
||
/** | ||
* Shrink capacity to match current size | ||
* Use with precaution as it defeats the purpose of amortized growth | ||
*/ | ||
void shrink_to_fit() | ||
{ | ||
m_data.resize(m_size); | ||
m_data.shrink_to_fit(); | ||
} | ||
size_t size() const noexcept { return m_size; } | ||
size_t capacity() const noexcept { return m_data.size(); } | ||
|
||
/** | ||
* @brief Direct access to the underlying data pointer. | ||
* @return Pointer to the first element (may be null if capacity()==0). | ||
* | ||
* This be used by us for using the dynamic array with numpy | ||
*/ | ||
T *get_data_ptr() noexcept { return m_data.data(); } | ||
const T *get_data_ptr() const noexcept { return m_data.data(); } | ||
|
||
T &operator[](size_t idx) noexcept { return m_data[idx]; } | ||
const T &operator[](size_t idx) const noexcept { return m_data[idx]; } | ||
}; | ||
|
||
/** | ||
* @brief A two-dimensional dynamic array backed by a flat, row-major buffer. | ||
* | ||
* Stores data in a single contiguous std::vector<T> to match C-style and NumPy | ||
* memory layout, enabling zero-copy interop (e.g., via Cython). | ||
* Supports amortized , O(1) growth in both dimensions and efficient shrinking. | ||
*/ | ||
template <class T> | ||
class DynamicArray2D | ||
{ | ||
int old_n, old_m; | ||
std::vector< std::vector<T>* > data; | ||
private: | ||
mstimberg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
std::vector<T> m_buffer; // Underlying flat buffer (capacity = allocated slots) | ||
size_t m_rows; // Logical number of rows exposed to the user | ||
size_t m_cols; // Logical number of columns exposed to the user | ||
size_t m_buffer_rows; // Physical buffer row capacity | ||
size_t m_buffer_cols; // Physical buffer column capacity (stride) | ||
double m_growth_factor; // Grow multiplier to reduce realloc frequency | ||
|
||
/** | ||
* Convert 2D coordinates to flat index | ||
* Row-major: i.e. elements of same row are contiguous | ||
*/ | ||
inline size_t index(size_t i, size_t j) const | ||
{ | ||
assert(i < m_buffer_rows && j < m_buffer_cols); | ||
return i * m_buffer_cols + j; | ||
} | ||
|
||
public: | ||
int n, m; | ||
DynamicArray2D(int _n=0, int _m=0) | ||
{ | ||
old_n = 0; | ||
old_m = 0; | ||
resize(_n, _m); | ||
}; | ||
~DynamicArray2D() | ||
{ | ||
resize(0, 0); // handles deallocation | ||
} | ||
void resize() | ||
{ | ||
if(old_n!=n) | ||
{ | ||
if(n<old_n) | ||
{ | ||
for(int i=n; i<old_n; i++) | ||
{ | ||
if(data[i]) delete data[i]; | ||
data[i] = 0; | ||
} | ||
} | ||
data.resize(n); | ||
if(n>old_n) | ||
{ | ||
for(int i=old_n; i<n; i++) | ||
{ | ||
data[i] = new std::vector<T>; | ||
} | ||
} | ||
if(old_m!=m) | ||
{ | ||
for(int i=0; i<n; i++) | ||
data[i]->resize(m); | ||
} else if(n>old_n) | ||
{ | ||
for(int i=old_n; i<n; i++) | ||
data[i]->resize(m); | ||
} | ||
} else if(old_m!=m) | ||
{ | ||
for(int i=0; i<n; i++) | ||
{ | ||
data[i]->resize(m); | ||
} | ||
} | ||
old_n = n; | ||
old_m = m; | ||
}; | ||
void resize(int _n, int _m) | ||
{ | ||
n = _n; | ||
m = _m; | ||
resize(); | ||
} | ||
// We cannot simply use T& as the return type here, since we don't | ||
// get a bool& out of a std::vector<bool> | ||
inline typename std::vector<T>::reference operator()(int i, int j) | ||
{ | ||
return (*data[i])[j]; | ||
} | ||
inline std::vector<T>& operator()(int i) | ||
{ | ||
return (*data[i]); | ||
} | ||
// We keep these for backwards compatibility | ||
size_t &n = m_rows; | ||
size_t &m = m_cols; | ||
|
||
DynamicArray2D(size_t rows = 0, size_t cols = 0, double factor = 2.0) | ||
: m_rows(rows), m_cols(cols), | ||
m_buffer_rows(rows), m_buffer_cols(cols), | ||
m_growth_factor(factor) | ||
{ | ||
m_buffer.resize(m_buffer_rows * m_buffer_cols); | ||
} | ||
/** | ||
* @brief Legacy constructor | ||
*/ | ||
DynamicArray2D(int _n, int _m) | ||
: DynamicArray2D(static_cast<size_t>(_n), | ||
static_cast<size_t>(_m), | ||
2.0) {} | ||
|
||
~DynamicArray2D() = default; | ||
|
||
/** | ||
* @brief Resize the array to new_rows x new_cols, preserving as much data as possible. | ||
* @param new_rows The desired number of logical rows. | ||
* @param new_cols The desired number of logical columns. | ||
* | ||
* If the requested size is larger than the current buffer, we grow the | ||
* internal storage using an over-allocation strategy: | ||
* new_dim = max(requested, old_capacity * growth_factor + 1) | ||
* for each dimension. This reduces the number of reallocations over time | ||
* and provides amortized O(1) growth. | ||
* | ||
* When resizing down (shrinking), we *don’t* free memory immediately. | ||
* Instead, we simply zero out the parts of the buffer that are now | ||
* outside the logical size. To actually release unused memory, | ||
* call `shrink_to_fit()`. | ||
*/ | ||
void resize(size_t new_rows, size_t new_cols) | ||
Legend101Zz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
bool needs_realloc = false; | ||
size_t grow_rows = m_buffer_rows; | ||
size_t grow_cols = m_buffer_cols; | ||
|
||
// First we check if buffer needs to grows | ||
if (new_rows > m_buffer_rows) | ||
{ | ||
size_t candidate = static_cast<size_t>(m_buffer_rows * m_growth_factor) + 1; | ||
grow_rows = std::max(new_rows, candidate); | ||
needs_realloc = true; | ||
} | ||
if (new_cols > m_buffer_cols) | ||
{ | ||
size_t candidate = static_cast<size_t>(m_buffer_cols * m_growth_factor) + 1; | ||
grow_cols = std::max(new_cols, candidate); | ||
needs_realloc = true; | ||
} | ||
|
||
if (needs_realloc) | ||
{ | ||
// Allocate new buffer and copy existing data | ||
std::vector<T> new_buf(grow_rows * grow_cols); | ||
size_t copy_rows = std::min(m_rows, new_rows); | ||
size_t copy_cols = std::min(m_cols, new_cols); | ||
|
||
for (size_t i = 0; i < copy_rows; ++i) | ||
{ | ||
for (size_t j = 0; j < copy_cols; ++j) | ||
{ | ||
new_buf[i * grow_cols + j] = m_buffer[index(i, j)]; | ||
} | ||
} | ||
// Swap in the new buffer and update capacities | ||
m_buffer.swap(new_buf); | ||
m_buffer_rows = grow_rows; | ||
m_buffer_cols = grow_cols; | ||
} | ||
else if (new_rows < m_rows || new_cols < m_cols) | ||
{ | ||
// Efficiently clear only the unused region without reallocating | ||
// Zero rows beyond new_rows | ||
for (size_t i = new_rows; i < m_buffer_rows; ++i) | ||
{ | ||
size_t base = i * m_buffer_cols; | ||
std::fill(&m_buffer[base], &m_buffer[base + m_buffer_cols], T(0)); | ||
} | ||
// Zero columns beyond new_cols in remaining rows | ||
for (size_t i = 0; i < new_rows; ++i) | ||
{ | ||
size_t base = i * m_buffer_cols + new_cols; | ||
std::fill(&m_buffer[base], &m_buffer[base + (m_buffer_cols - new_cols)], T(0)); | ||
} | ||
} | ||
|
||
// Finally, we update logical dimensions | ||
m_rows = new_rows; | ||
m_cols = new_cols; | ||
} | ||
|
||
// Legacy overloads for compatibility | ||
void resize(int new_n, int new_m) | ||
{ | ||
resize(static_cast<size_t>(new_n), static_cast<size_t>(new_m)); | ||
} | ||
|
||
void resize() | ||
{ | ||
resize(m_rows, m_cols); | ||
} | ||
|
||
/** | ||
* Shrink buffer to exact size | ||
* Warning: Invalidates pointers and defeats growth optimization | ||
*/ | ||
void shrink_to_fit() | ||
{ | ||
if (m_rows < m_buffer_rows || m_cols < m_buffer_cols) | ||
{ | ||
std::vector<T> new_buffer(m_rows * m_cols); | ||
|
||
// Copy data to compact buffer | ||
for (size_t i = 0; i < m_rows; ++i) | ||
{ | ||
if (std::is_trivially_copyable<T>::value) | ||
{ | ||
std::memcpy(&new_buffer[i * m_cols], &m_buffer[index(i, 0)], m_cols * sizeof(T)); | ||
} | ||
else | ||
{ | ||
for (size_t j = 0; j < m_cols; ++j) | ||
{ | ||
new_buffer[i * m_cols + j] = m_buffer[index(i, j)]; | ||
} | ||
} | ||
} | ||
|
||
m_buffer.swap(new_buffer); | ||
m_buffer_rows = m_rows; | ||
m_buffer_cols = m_cols; | ||
} | ||
} | ||
|
||
// Dimension getters | ||
size_t rows() const noexcept { return m_rows; } | ||
size_t cols() const noexcept { return m_cols; } | ||
size_t stride() const noexcept { return m_buffer_cols; } // for numpy stride calculationx | ||
|
||
/** | ||
* Raw data access for numpy integration | ||
* Returns pointer to start of buffer | ||
* Note: stride() != cols() when buffer is over-allocated | ||
*/ | ||
T *get_data_ptr() noexcept { return m_buffer.data(); } | ||
const T *get_data_ptr() const noexcept { return m_buffer.data(); } | ||
|
||
// 2D element access, no bounds checking for speed. | ||
inline T &operator()(size_t i, size_t j) noexcept { return m_buffer[index(i, j)]; } | ||
inline const T &operator()(size_t i, size_t j) const noexcept { return m_buffer[index(i, j)]; } | ||
|
||
// Overloads for int indices for backward compatibility. | ||
inline T &operator()(int i, int j) noexcept { return operator()(static_cast<size_t>(i), static_cast<size_t>(j)); } | ||
inline const T &operator()(int i, int j) const noexcept { return operator()(static_cast<size_t>(i), static_cast<size_t>(j)); } | ||
|
||
/** | ||
* @brief Returns a copy of row i as std::vector<T>. | ||
* @note This is a copy; for slicing without copy, consider returning a view. | ||
Legend101Zz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
*/ | ||
std::vector<T> operator()(size_t i) const | ||
{ | ||
std::vector<T> row(m_cols); | ||
for (size_t j = 0; j < m_cols; ++j) | ||
{ | ||
row[j] = m_buffer[index(i, j)]; | ||
} | ||
return row; | ||
} | ||
}; | ||
|
||
#endif |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.