You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
FixedCircularBuffer isFull(), isEmpty() and wrapping off by one #53139
How often does it reproduce? Is there a required condition?
No response
What is the expected behavior? Why is that the expected behavior?
No response
What do you see instead?
Implementation copied from lib/internal/fixed_queue.js:
constkSize=4;constkMask=kSize-1;classFixedCircularBuffer{constructor(){this.bottom=0;this.top=0;this.list=newArray(kSize);this.next=null;}isEmpty(){returnthis.top===this.bottom;}isFull(){return((this.top+1)&kMask)===this.bottom;}push(data){this.list[this.top]=data;this.top=(this.top+1)&kMask;}shift(){constnextItem=this.list[this.bottom];if(nextItem===undefined)returnnull;this.list[this.bottom]=undefined;this.bottom=(this.bottom+1)&kMask;returnnextItem;}}constcircularBuffer=newFixedCircularBuffer();console.log(circularBuffer.isFull());// falseconsole.log(circularBuffer.isEmpty());// truecircularBuffer.push(1);console.log(circularBuffer.list);console.log(circularBuffer.isFull());// falseconsole.log(circularBuffer.isEmpty());// falsecircularBuffer.push(2);console.log(circularBuffer.list);console.log(circularBuffer.isFull());// falseconsole.log(circularBuffer.isEmpty());// falsecircularBuffer.push(3);console.log(circularBuffer.list);console.log(circularBuffer.isFull());// false, received trueconsole.log(circularBuffer.isEmpty());// falsecircularBuffer.push(4);console.log(circularBuffer.list);console.log(circularBuffer.isFull());// true, received falseconsole.log(circularBuffer.isEmpty());// false, received true
One way to fix it to count the number of elements in list at push() and shift() and test it against zero or the 'list` fixed array size.
It's current usage in FixedQueue does not expose the issue with wrapping when full and the resulting incorrect FIFO semantic at shift() because a new buffer is created. But the trade off at losing one array space each 2047 elements is debatable vs. integer property increment/decrement + branching one more.
changed the title [-]FixedCircularBuffer isFull(), isEmpty() off by one[/-][+]FixedCircularBuffer isFull(), isEmpty() and wrapping off by one[/+]on May 24, 2024
Your example is demonstrating how buffer overflow exist. When people is pushing to a buffer stack without caring the state, then all the information will be messed up.
In a proper case, the buffer should throw when pushing new data in full state. So, the .push(4) is not possible.
But, if everyone knows what he is doing it can be skipped.
For example, the below usage shows it is properly skip pushing when a FixedCircularBuffer is full.
The expected circular buffer semantic is not to throw when full. Making it throw will follow a fixed queue semantic.
As I said, the current usage is not triggering the issue. And the buggy implementation is not exported. I understand the branching less optimization for a specific use case.
But the tradeoff is debatable with for example a 100k sized fixed queue. And I do not find any benchmarking code on fixed queue implementation in the repo.
Before I start to test optimization in that code with appropriate benchmarking, I want to make sure the semantic issues on well defined data structure are understood. The naming fixed queue on in fact a queue using several fixed arrays for storage is also debatable.
A circular buffer makes a bounded queue when separate indices are used for inserting and removing data. The queue can be safely shared between threads (or processors) without further synchronization so long as one processor enqueues data and the other dequeues it. (Also, modifications to the read/write pointers must be atomic, and this is a non-blocking queue--an error is returned when trying to write to a full queue or read from an empty queue).
Despite it is expected for throwing in circular buffer or not.
I still incline to think that when circular buffer is implemented for queue, it should throw when the buffer is full.
Otherwise, the queue will be overwritten.
For the usage of FixedCircularBuffer in FixedQueue.
Note that a circular buffer with n elements is usually used to implement a queue with n-1 elements--there is always one empty element in the buffer. Otherwise, it becomes difficult to distinguish between a full and empty queue--the read and write pointers would be identical in both cases.
I believe it is acceptable to be n - 1 in actual size.
The implementation of FixedQueue using multiple FixedCircularBuffer is based on the optimal buffer size 2048 in v8 and allows unbounded queue size.
The original design is date back from PR 18617, throughout the conversation. The benefit are
Reduce GC times
Prevent object creation
Note: I am not a member from Node.js, I just want to discuses about it since I am interested in the implementation too.
A circular buffer makes a bounded queue when separate indices are used for inserting and removing data. The queue can be safely shared between threads (or processors) without further synchronization so long as one processor enqueues data and the other dequeues it. (Also, modifications to the read/write pointers must be atomic, and this is a non-blocking queue--an error is returned when trying to write to a full queue or read from an empty queue).
Despite it is expected for throwing in circular buffer or not. I still incline to think that when circular buffer is implemented for queue, it should throw when the buffer is full. Otherwise, the queue will be overwritten.
A fixed queue can be implemented with a circular buffer design, at the cost of losing one 'space'. In that case, it's still called a fixed queue, not a circular buffer. The underlying design choice to implement a fixed queue semantic is not expected to change the name of the initial abstraction.
The usage of the term 'circular' expects a wrapping semantic when full.
The nitpicking on the naming is not that important here. One can find in the wild different definitions.
My concerns are more about the performance optimization of the implementation without benchmarking the alternatives:
Fixed array direct usage vs. fixed circular buffer (fixed queue in fact): native push()/shift() on array vs. class maintaining counters for the fixed array indexes
Bitwise operations vs. ternary operator for counters maintenance
I plan when time permits to add a benchmark for the fixed queue. Then start testing with performance regression detection some micro optimizations.
My concerns are more about the performance optimization of the implementation without benchmarking the alternatives:
Fixed array direct usage vs. fixed circular buffer (fixed queue in fact): native push()/shift() on array vs. class maintaining counters for the fixed array indexes
On that one, the current implementation is the most efficient and cannot change the size of the fixed size array without noticing.
Bitwise operations vs. ternary operator for counters maintenance
On that one, it's not that clear, the integer binary representation significant bits size seems to impact badly bitwise operation at a first glance, when the ternary operator behaves most consistently whatever its value is.
Activity
[-]FixedCircularBuffer isFull(), isEmpty() off by one[/-][+]FixedCircularBuffer isFull(), isEmpty() and wrapping off by one[/+]climba03003 commentedon May 27, 2024
Your example is demonstrating how buffer overflow exist. When people is pushing to a buffer stack without caring the state, then all the information will be messed up.
In a proper case, the buffer should throw when pushing new data in full state. So, the
.push(4)is not possible.But, if everyone knows what he is doing it can be skipped.
For example, the below usage shows it is properly skip pushing when a
FixedCircularBufferis full.node/lib/internal/fixed_queue.js
Lines 99 to 117 in ff659fa
jerome-benoit commentedon May 27, 2024
The expected circular buffer semantic is not to throw when full. Making it throw will follow a fixed queue semantic.
As I said, the current usage is not triggering the issue. And the buggy implementation is not exported. I understand the branching less optimization for a specific use case.
But the tradeoff is debatable with for example a 100k sized fixed queue. And I do not find any benchmarking code on fixed queue implementation in the repo.
Before I start to test optimization in that code with appropriate benchmarking, I want to make sure the semantic issues on well defined data structure are understood. The naming fixed queue on in fact a queue using several fixed arrays for storage is also debatable.
climba03003 commentedon May 27, 2024
Reference from https://wiki.c2.com/?CircularBuffer
Despite it is expected for throwing in circular buffer or not.
I still incline to think that when circular buffer is implemented for queue, it should throw when the buffer is full.
Otherwise, the queue will be overwritten.
For the usage of
FixedCircularBufferinFixedQueue.I believe it is acceptable to be
n - 1in actual size.The implementation of
FixedQueueusing multipleFixedCircularBufferis based on the optimal buffer size2048inv8and allows unbounded queue size.The original design is date back from PR 18617, throughout the conversation. The benefit are
Note: I am not a member from Node.js, I just want to discuses about it since I am interested in the implementation too.
jerome-benoit commentedon May 27, 2024
A fixed queue can be implemented with a circular buffer design, at the cost of losing one 'space'. In that case, it's still called a fixed queue, not a circular buffer. The underlying design choice to implement a fixed queue semantic is not expected to change the name of the initial abstraction.
The usage of the term 'circular' expects a wrapping semantic when full.
The nitpicking on the naming is not that important here. One can find in the wild different definitions.
My concerns are more about the performance optimization of the implementation without benchmarking the alternatives:
I plan when time permits to add a benchmark for the fixed queue. Then start testing with performance regression detection some micro optimizations.
jerome-benoit commentedon May 29, 2024
On that one, the current implementation is the most efficient and cannot change the size of the fixed size array without noticing.
On that one, it's not that clear, the integer binary representation significant bits size seems to impact badly bitwise operation at a first glance, when the ternary operator behaves most consistently whatever its value is.
Benchmark results to come.
jerome-benoit commentedon Jul 19, 2024
Not enough FOSS time to dig further.