Saturday, July 27, 2024

Thread local Stopgap

I made the stopgap solution thread local. It's still expensive.
   ~VMObjectArray() {
#ifdef GC_STOPGAP
        if (deferring) {
            for (int i = 0; i < _size; i++) {
                defer.push(_array[i]);
            }
            delete[] _array;
        } else {
            deferring = true;
            for (int i = 0; i < _size; i++) {
                defer.push(_array[i]);
            }
            delete[] _array;
            while (!defer.empty()) {
                defer.pop();
            }
            deferring = false;
        }
#else
        delete[] _array;
#endif
    }

Tuesday, July 16, 2024

Stopgap solution to Cascading Free

 Egel runtime objects are instances of C++'s std::shared_ptr. Because egel objects can be deeply nested, it is not uncommon for large structures to go out of scope and result in cascading frees that blow up the C stack.

I.e., the following short egel program would core dump:

# rewrite a list of a million numbers to none
from_to 0 1000000 |> [_ -> none]   
I implemented a costly stopgap solution. First, we need a collector for trash. It has an in queue and an out queue, and its sole intention is to retain objects for a while such that they don't go out of scope recursively and blow up the C stack.

class GC {

public:
    GC() {
    }

    void push(const VMObjectPtr &o) {
        std::lock_guard<std::mutex> lock(mtx_in);
        _in.push(o);
    }

    void copy() {
        std::scoped_lock lock(mtx_in, mtx_out);
        while (!_in.empty()) {
            _out.push(_in.top());
            _in.pop();
        }
    }

    bool done() {
        std::scoped_lock lock(mtx_in, mtx_out);
        return _in.empty() && _out.empty();
    }

    void empty() {
        std::lock_guard<std::mutex> lock(mtx_out);
        while (!_out.empty()) _out.pop();
    }

    void clear() {
        while (!done()) {
            copy();
            empty();
        }
    }

private:
    mutable std::mutex mtx_in;
    mutable std::mutex mtx_out;
    std::stack<vmobjectptr> _in;
    std::stack<vmobjectptr> _out;
};

inline GC garbage_collector;
Now when an array goes out of scope, we place its members in the collector.
    ~VMObjectArray() {
        for (int i = 0; i < _size; i++) {
            garbage_collector.push(_array[i]);
        }
        delete[] _array;
    }

And during rewrites we then empty the queues.
    while ((trampoline != nullptr) && (*run != HALTED)) {
        if (*run == RUNNING) {
            trampoline = f->reduce(trampoline);
            garbage_collector.clear();
        } else if (*run == SLEEPING) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        } else {  // *run == HALTED
        }
    }
I am not happy with this solution. First, I consider it a std::shared_ptr bug that it can blow up the C stack, and second, the performance cost is way too steep for my taste.

I'ld rather convince C++ developers that this is their bug; for all I know cascading frees are a normal thing in reference counted garbage collection and their implementation should be able to handle that.