Skip to content

Conversation

@niaow
Copy link
Member

@niaow niaow commented Jul 18, 2021

This memory manager uses a simple linked list to track active allocations, and measures the gaps between them to find available memory. Performance is not great for large heaps, but is sufficient for microcontrollers with single-digit kilobytes of memory.

This is effectively a simplified version of #1193 containing only the GC itself so that it is easier to review.

The ./testdata/gc.go test was run on an Arduino Uno and passes. It is slow.

The false-positive rate problem still applies to microcontrollers where the address space is mostly used.

@niaow
Copy link
Member Author

niaow commented Jul 18, 2021

Size comparison:

[niaow@finch tinygo]$ build/tinygo build -target=arduino -size short -gc=conservative -o /tmp/gc.hex ./testdata/gc.go 
   code    data     bss |   flash     ram
   3768      89     689 |    3857     778
[niaow@finch tinygo]$ build/tinygo build -target=arduino -size short -gc=list -o /tmp/gc.hex ./testdata/gc.go 
   code    data     bss |   flash     ram
   2712      94     686 |    2806     780

Copy link
Member

@aykevl aykevl left a comment

Choose a reason for hiding this comment

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

I looked at the code (not super in-depth though) and it looks reasonable to me. Thank you for this effort! This is indeed much easier to review when it's separated from a precise GC (which will be quite a bit of work and may also be useful for -gc=conservative).

Can you add a test for this GC? You can add a test in main_test.go, see the opt=1, opt=0, and ldflags tests.

// sort the allocation list in ascending address order.
func (list *allocList) sort() {
// Sort the list by repeatedly moving the lowest-address element to the front.
// This is selection sort (https://en.wikipedia.org/wiki/Selection_sort).
Copy link
Member

Choose a reason for hiding this comment

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

I'm slightly worried that a quadratic algorithm in a GC might be a problem. Maybe not on the AVR though with its very limited memory.

I think there is a way to avoid this sorting, at least if it's fine to limit the maximum object size to half the address space (which seems like a reasonable limitation to me). You can use one bit of allocHeader.len (e.g. the top bit) to store the mark state. Then, during sweeping, you can iterate through all objects and remove the ones that haven't been marked.

Copy link
Member Author

Choose a reason for hiding this comment

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

That seems reasonable.

Copy link
Member

Choose a reason for hiding this comment

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

It's just an idea for how it can be improved. The code as-is already looks good to me.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, I think this may be better as-is. That tagging strategy would actually also make the scan quadratic (the list needs to be fully traversed each time we need to find what to scan). I think the current strategy with the scan stack is at least a bit more readable.

}

// Search the list of unmarked allocations for this root address.
alloc := activeAllocs.search(root)
Copy link
Member

Choose a reason for hiding this comment

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

This also looks like a quadratic algorithm here: for every possible root, all allocations are scanned.

Copy link
Member Author

@niaow niaow Jul 19, 2021

Choose a reason for hiding this comment

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

Unfortunately yes. Not sure if this can/should be changed. This worked much better with the precise GC because there were far fewer roots to deal with. This is also fairly reasonable on AVR where I don't really see more than a dozen allocations very often.


// findMem searches the heap for a free span large enough to contain an allocation of the specified size.
// If there are no sufficiently large free spans available, this returns nil.
func findMem(size uintptr) *allocHeader {
Copy link
Member

@aykevl aykevl Jul 19, 2021

Choose a reason for hiding this comment

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

Two things I realize now:

  • The memory should be aligned. You can use the align function for this. This is a no-op on AVR and should be optimized away entirely. On other platforms, it is necessary for correct functioning.

    size = align(size)
  • You might want to use a zeroSizedAlloc global (like in gc_conservative.go) to return for when the size is zero. This does sometimes happen in practice, although it is rare. The Go spec explicitly allows this:

    Pointers to distinct zero-size variables may or may not be equal.

    On the other hand, it's rare and costs RAM/flash, so it would be reasonable to leave this out.

Copy link
Member Author

Choose a reason for hiding this comment

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

Adding the 0-sized alloc in because it only costs 1 byte of RAM (and saves 28 bytes of flash for some reason).

Copy link
Member Author

Choose a reason for hiding this comment

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

I am not entirely sure what you mean with the alignment. Assuming that heapStart is aligned (which I picked up from the conservative collector), findMem should always return appropriately aligned data. The header preserves pointer/word alignment, and every subsequent start is corrected with an align call.

This memory manager uses a simple linked list to track active allocations, and measures the gaps between them to find available memory.
Performance is not great for large heaps, but is sufficient for microcontrollers with single-digit kilobytes of memory.
@niaow
Copy link
Member Author

niaow commented Jul 29, 2021

@aykevl I am currently planning to do some GC cleanup to try to ensure that all of the conservative GCs behave similarly. Should we merge this first or should I push this to later?

@deadprogram deadprogram requested a review from aykevl August 4, 2021 20:03
@deadprogram
Copy link
Member

Either way, please note merge conflicts @niaow

@niaow
Copy link
Member Author

niaow commented Nov 16, 2021

I am going to close this. This GC has too many issues and we have other priorities right now.

@niaow niaow closed this Nov 16, 2021
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