Skip to content

[ready for review] btreeg: add support for no alloc iter#52

Merged
tidwall merged 8 commits intotidwall:masterfrom
canibeanartisttoo:arjun/new_branch_test
Nov 3, 2025
Merged

[ready for review] btreeg: add support for no alloc iter#52
tidwall merged 8 commits intotidwall:masterfrom
canibeanartisttoo:arjun/new_branch_test

Conversation

@arjunnair1997
Copy link
Contributor

@arjunnair1997 arjunnair1997 commented Aug 7, 2025

This pr introduces the following API:

// ReleaseReuseable is the same as Release, but it preserves the iterator stack
// so that the iterator can be reused using Init without allocating.
func (iter *IterG[T]) ReleaseReuseable() {
	if iter.tr == nil {
		return
	}
	if iter.locked {
		iter.tr.unlock(iter.mut)
		iter.locked = false
	}

	// Preserve the backing memory for the stack, so that the iterator can be re-used without
	// allocating.
	iter.stack = iter.stack[:0]
	iter.tr = nil
}

// Init is used to initialize an existing iterator with a new tree. ReleaseReusable must've
// been called on the iterator before re-using it using Init.
func (iter *IterG[T]) Init(tr *BTreeG[T], mut bool) {
	iter.tr = tr
	iter.mut = mut

	iter.locked = tr.lock(iter.mut)
	if iter.stack == nil {
		iter.stack = iter.stack0[:0]
	} else {
		iter.stack = iter.stack[:0]
	}

	iter.seeked = false
	iter.atstart = false
	iter.atend = false
	iter.item = tr.empty
}

First, an iterator is created using the BtreeG.Iter or IterMut method. Then, the iterator can be re-used by first calling ReleaseReusable, and then calling Init.

This prevents two types of allocations:

  1. To grow the iter.stack slice.
  2. The iterator can escape to the heap as included in the benchmark.

@arjunnair1997 arjunnair1997 changed the title [WIP] add support for no alloc iter btreeg: add support for no alloc iter Aug 12, 2025
@arjunnair1997 arjunnair1997 changed the title btreeg: add support for no alloc iter [ready for review] btreeg: add support for no alloc iter Aug 12, 2025
@tidwall
Copy link
Owner

tidwall commented Aug 14, 2025

Is the goal here to have a zero alloc iterator?
Changing the Init behavior, like you've shown, does omits the heap escape, but there will still be escapes in the future with the cursor functions First/Last/Next/Prev.

@arjunnair1997
Copy link
Contributor Author

Is the goal here to have a zero alloc iterator? Changing the Init behavior, like you've shown, does omits the heap escape, but there will still be escapes in the future with the cursor functions First/Last/Next/Prev.

This is the only alloc which is significant enough to show up in our profiles. I'm just using a seek + next.

@tidwall
Copy link
Owner

tidwall commented Aug 15, 2025

Can you provide a little more info about what you are seeing?
Perhaps some code to reproduce the issue?
I'm running with and without Iter() and IterNoAlloc() and I'm not seeing any difference.

@tidwall
Copy link
Owner

tidwall commented Aug 18, 2025

It just occurred to me, but if you are only using Seek and Next, you may want to consider using the Ascend function. It has a much lower footprint, doesn't touch the heap, and is generally faster than using the Iter functions.

@arjunnair1997
Copy link
Contributor Author

arjunnair1997 commented Sep 8, 2025

@tidwall

I can't use ascend as I need to do a specific kind of iteration. Let's say the keys are k1 < k2 < k3 < k4. If I have have a pivot key k_x such that k_2 < k_x < k_3, then I need my ascension to start from k_2 and not k_3. Ascend would start the iteration at k_3. Right now, we use Descend + Ascend combo for this, but that's too expensive. First descend to find the first key <= k_x, and then Ascend using k_x as pivot.

I don't want to use an iterator either, I'd rather just use a function like ascend which starts iteration one key prior the first key which is >=.

Basically what I want is Descend + scan forward which does a single seek to find the item to start the scan at(k_2 in the above example), and the scans forward like ascend. I can implement this if you're open to it.

What do you think?

@tidwall
Copy link
Owner

tidwall commented Sep 15, 2025

So perhaps functions similar to Ascend and Descend, that starts at a pivot, but allows for moving forward and backwards like cursor? That's interesting actually.

I'm seeing something like: (not sure of the name yet)

const (
    Stop Action = iota
    Prev
    Next
)

func (tr *BTree[T]) IterAscend(pivot T, iter(item T) Action)
func (tr *BTree[T]) IterDescend(pivot T, iter(item T) Action)
func (tr *BTree[T]) IterScan(iter(item T) Action)
func (tr *BTree[T]) IterReverse(iter(item T) Action)

These could work like Ascend, Descend, Scan, and Reverse, but instead or returning true or false, you can return Stop, Next, or Prev.

This will allow to start at any pivot and change direction on the fly.

@arjunnair1997
Copy link
Contributor Author

arjunnair1997 commented Oct 29, 2025

@tidwall Sorry about the delay.

I've made a lot of changes to this pr, it's a lot different from what we were discussing. I've included a benchmark which shows the allocation with the regular iterators. Then, I've introduced a new api to use the iterator without any allocations.

Please take a look whenever you get the chance. The changes and the reasoning is described here: #52 (comment)

I like the idea you mentioned here: #52 (comment), I've never seen an API like that before and it's pretty interesting, but just using the existing iterator is sufficient for our use case.

@tidwall
Copy link
Owner

tidwall commented Oct 31, 2025

Overall LGTM.

Though I am wondering if we can simply replace the existing iter.Release() with your new iter.ReleaseReusable()?
That way we have a one Release() function. Not sure if there's a side effect I'm not seeing.

@arjunnair1997
Copy link
Contributor Author

@tidwall Please take a look. Re-using Release, and made the re-use of the iterator safer by zeroing out memory.

@tidwall tidwall merged commit da3712b into tidwall:master Nov 3, 2025
3 checks passed
@tidwall
Copy link
Owner

tidwall commented Nov 3, 2025

Looks good!

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.

3 participants