technoblogy / ulisp

A version of the Lisp programming language for ATmega-based Arduino boards.

Home Page:http://www.ulisp.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

allow for dynamic memory allocation

dragoncoder047 opened this issue · comments

I had an idea...

Some of the platforms (esp, teensy, etc.) support malloc(). Could uLisp be updated so that it can take advantage of dynamic memory allocation?

The way it could work is:

  • WORKSPACESIZE becomes a size_t variable instead of a #define
  • Workspace does not have a declared size
  • After a GC it checks:
    • if the workspace is 1/4 full or less the workspace is compactimage()'d and the size is halved using realloc()
    • if the workspace is 3/4 full or more the workspace size is doubled using realloc()
    • either case if the workspace chunk was moved by realloc() then uLisp uses movepointer() to fix everything
    • maybe set a minimum workspace size of 128 objects

This would allow the uLisp program to coexist more easily with other systems -- it will release memory if it doesn't need it and if realloc() fails and the workspace is full then it can call out of memory error. That way you don't have to use a huge #if cascade to empirically find out how much memory is appropriate for each platform -- it will use as much as it needs.

How does that sound?

Thanks for thinking about uLisp!

I assume you're thinking that we would use malloc() to allocate a large contiguous chunk of memory, and then use this like the workspace is currently used.

I must admit that I'm not a fan of malloc(). I much prefer to be in control of what memory I have available, and how it is allocated. I'm not sure malloc() runs very reliably on many of the platforms that uLisp supports (the ESP32 is probably one of the better ones). And I think there are also some problems with ensuring that malloc() will allocate memory on a 4-byte boundary, and that's important for uLisp.

Using compactimage() and movepointer() to resize the workspace or fix relocation would be very inefficient, as these are quite slow. It's not a problem where they are used in saveimage, because this is called infrequently, and from the REPL, but as part of GC I think it would have an impact.

There isn't really a huge #if cascade for how much memory is needed for each platform; just one number as part of the other configuration details, such as the board identifier. The way it works at the moment it can give a graceful out of memory message, without corrupting the workspace; would that still be possible with malloc()?

Finally, I think it would be quite a bit of work, and I'm not sure coexisting with other systems is a major benefit.

However, I don't want to be discouraging - by all means try it yourself, and perhaps you will persuade me to change my mind!

I must admit that I'm not a fan of malloc(). I much prefer to be in control of what memory I have available, and how it is allocated. I'm not sure malloc() runs very reliably on many of the platforms that uLisp supports (the ESP32 is probably one of the better ones). And I think there are also some problems with ensuring that malloc() will allocate memory on a 4-byte boundary, and that's important for uLisp.

Actually I think that all you need is a 2-byte boundary since all you need is the MARKBIT to be what you want. Also I have heard that almost all processors allocate memory by aligning it to a multiple of the bit width greater than the chunk size (so 32 bits). So I think you're good there.

Never mind, I figured out why it needs to be 4 bytes...

Using compactimage() and movepointer() to resize the workspace or fix relocation would be very inefficient, as these are quite slow. It's not a problem where they are used in saveimage, because this is called infrequently, and from the REPL, but as part of GC I think it would have an impact.

I did worry about that but if you adjust the thresholds for resizing (e.g. only shrink the workspace when it is only 1/16 used instead of 1/4, only grow it when it is 15/16 full, etc) you will not need to call movepointer() much at all because programs' usage of memory doesn't change that fast. There could also be some other heuristic that if the workspace usage remains low for so many garbage collections, then compact the workspace. This would only do stuff in the REPL or if the program calls GC explicitly.

Also the other concern that compactimage() is slow, would not be needed in the case of making the workspace bigger. If realloc() has to move the workspace then all you have to do is sweep the low end and use movepointer() to update the pointers by the same amount that realloc() moved the workspace by. movepointer() only has O(n^2) complexity, while compactimage() has O(n^3) as far as I can see.

The way it works at the moment it can give a graceful out of memory message, without corrupting the workspace; would that still be possible with malloc()?

Sure, it would be able to do that. I am not saying to change how myalloc() works. It can still pull from the Workspace/Freelist. The Workspace itself would be allocated by one giant malloc call.

The advantage of this especially on esp's is that ones that have PSRAM can automatically put the Workspace over into PSRAM once it gets big enough. There is a function somewhere that tells it the threshold where it should put allocations in internal RAM (faster, smaller) versus PSRAM (bigger, slower).

Yes, Kaef used malloc() to add support for PSRAM on the ESP32-WROVER, though not with the additional features you've suggested:

http://forum.ulisp.com/t/ulisp-2-4-esp-with-4-mbyte-psram-support/228

To put it more clearly:

object* Workspace;
size_t WORKSPACESIZE = 128;

void gc () {
    /* snip  everything except sweep() */
    object* old_loc = Workspace;
    size_t oldsz = WORKSPACESIZE;
    if (Freeespace < WORKSPACESIZE / 16 && WORKSPACESIZE > 128) {
        compactimage();
        old_loc = Workspace;
        WORKSPACESIZE /= 2;
        Workspace = (object*)realloc((void*)Workspace, WORKSPACESIZE * sizeof(object));
    }
    else if (WORKSPACESIZE - Freeespace < WORKSPACESIZE / 16) {
        old_loc = Workspace;
        WORKSPACESIZE *= 2;
        Workspace = (object*)realloc((void*)Workspace, WORKSPACESIZE * sizeof(object));
        if (Workspace == NULL) Workspace = old_loc;
    }
    /* sweep() here */
    if (Workspace != old_loc) {
        for (size_t i = 0; i < oldsz; i++) movepointer(old_loc[i], Workspace[i]);
    }
}

void initworkspace () {
    Workspace = (object*)malloc(WORKSPACESIZE * sizeof(object));
    /* snip */
}

You might be able to just cut-and-paste this in. Let me know if it works!

Why don't you try it? As I said, I'm not really convinced about the benefits, but I'd be interested to know.

Not that I would need it, but I just realized that using malloc() would break saveimage() because malloc returns an unpredictable pointer. So maybe it's not a good idea until something like #60 is implemented.