On programming without malloc
jvns.ca
jvns.ca
For example, it used to bug me that I had to statically allocate memory to manage an event that was used for maybe 0.1% of the product's life. It seemed so inefficient. Yes, you could potentially co-opt the event and use it elsewhere, but then you had to deal with a bunch of other considerations: could they ever run at the same time?, would the code be maintainable?, etc.
Or the other day I accidentally set a function scoped buffer's size too large and it cause a stack pointer overflow. That was a pain to debug, because the exception happened "before" the function started running (in the function call preamble). From the debugger, it looked like the return of the previous function caused the issue.
Where do you work?
(Embedded programming is really quite nice to teach clean C, you learn to appreciate what the cost of library functionality is, that you can't just start using floats, that on some architectures updating a 32 bit integer is a non-atomic operation spanning 5 or more clock cycles..)
Maybe it's because quite many years ago I used to do some very narrow scope algorithms in C for some code that needed high reliability.
How could such code even have mallocs? You would need to then know that at no point the memory requested would be greater than memory available. So if you already know that, why not allocate the memory beforehand?
Your example, a feature used for 0.1% of product life. Certainly you couldn't tolerate that the software hung to a memory error 0.1% of the time? Hence if the product supports the feature, then it must always work, and thus memory must always be reserved for it. I don't understand how it would really work otherwise.
The other end of the spectrum, the "modern way" even in high performance tasks, with reckless java object creation and jvm memory shuffling almost makes me sick. Then everybody's tuning the garbage collectors to avoid constant full gc. It's heuristics. Oracle makes drastic changes to the GC defaults between minor releases.
While there perhaps aren't maybe many direct memory bugs in such code, the code does become very unpredictable.
Then to fix that it's again going to preallocated static pools and primitives and self implemented cache cleaning and what else, and you've lost quite a lot of your java style.
void *malloc(int size) {
static long nextAddress = BASE_ADDRESS;
void *ptr = (void *)nextAddress;
nextAddress += size;
return ptr;
}
void free(void *ptr) {
return;
}
Using that will last you a long time in writing a toy operating system kernel. Of course, it will eventually run out of memory pages, but that shouldn't happen until your system has been running for a long time.But it's perfectly correct: English writers have been using both the singular 'they' continuously for hundreds of years (14th century?). It also has equivalents in other west European languages (e.g. the German neuter gender and the French ils for a group of people of indeterminate gender), so it's not an English invention either.
All of those things are no longer true, based on my casual reading of the New York Times.
(Note - I was also told to use "He" as the generic case for singular person. Outside of english 11/12, I've never used anything other than "they" - and have never had anyone comment on it. "They" as the singular person form is fine, and avoids the gender confusion.)
If you know someone rather well, you say
"Wie geht es dir" - "How are you?"
but if you don't know him, you say
"Wie geht es ihnen?", which literally means "How are they?"
Also, back in the old days, you would talk to the nobility in plural.
"Wie geht es euch?", there is no equivalent in English I guess, since you is plural and singular, "How are you?"
And it's totally fine "They are" has been used in the way for hundreds of years.
"Though semantically singular or ambiguous, singular they remains morphologically and syntactically plural (e.g. it still takes plural forms of verbs)."
Thanks.
Second, no I should not assume. Women and tech have a complex enough relationship as is, and I fully support making our industry more inclusive
Third, you should not assume either. A name is not enough to determine someone's sex or gender. It is then safest to use "they" or even ze or zhe [1], though obviously very few people do. I also hate to be pedantic :).
[1] http://en.m.wikipedia.org/wiki/Gender-specific_and_gender-ne...
It really is an important point, not pedantry, to not assume based on names because many cultures have names that sound like the other gender to American ears (and vice versa, no doubt).
It can do that, it's not required to.
> Also, sbrk() is a syscall, so it's not available in OP's use case: he has not implemented it.
Yes, they have, they just called it malloc() :-). Which was really my (admittedly fairly terse) point.
typedef union { char irrelevant[ALIGNMENT]; double d; } aligned;
static aligned realspace[SPACE / ALIGNMENT];
#define space ((char *) realspace)
static unsigned int avail = SPACE; /* multiple of ALIGNMENT; 0<=avail<=SPACE */
char *alloc(n)
unsigned int n;
{
n = ALIGNMENT + n - (n & (ALIGNMENT - 1)); /* XXX: could overflow */
if (n <= avail) {
avail -= n;
return space + avail;
}
return malloc(n);
}
void alloc_free(x)
char *x;
{
if (x >= space && x < space + SPACE) {
return; /* XXX: assuming that pointers are flat */
}
free(x);
}Mine will eventually wrap around and clobber whatever's at 0x0. It assumes you have a very liberal page allocator in the background to handle page faults for whatever addresses you decide to point to and automatically wire them up to RAM.
Programming without malloc is a good exercise and can also be valuable outside of kernel space. Computer programs written in a naive manner may end up spending most of their time doing memory allocations. This seldom happens in C programs because usually you're aware of every malloc you do. But I've had a few Python programs end up uselessly slow because of the time wasted in allocating small objects. I was doing some physics simulation prototyping with Python and the result was too slow to do work in (soft) realtime. (although you can avoid this, you'd be writing non-idiomatic Python code and that practically destroys any benefit of using it for prototyping).
@jvns: do you have the source code of your mini-kernel project shared anywhere? Any plans for the future?
https://github.com/rikusalminen/danjeros this is my hobby kernel project from a few years ago.
[1]: https://github.com/charliesome/rustboot [2]: https://github.com/jvns/rustboot
This isn't really true, it turns out :) I've only spent about a week using Rust. I spend a ton of time in #rust asking questions, and everyone's been really helpful. The maintainer of rust-core (the library that lets you not require runtime support) hangs out in #rust all the time and has been wonderful about answering all my questions about it.
> This isn't really true, it turns out :) I've only spent about a week using Rust. I spend a ton of time in #rust asking questions, and everyone's been really helpful.
Rust is perhaps easier in this aspect than pretty much any other high level language out there. The fact that Rust can operate without a fully featured runtime system makes it one of the most interesting new languages in my opinion. The different "pointer types" in Rust make this possible, it's a bit confusing to start with but enables nice things on the other hand.
It is nice to see someone exploring alternatives to memory management other than malloc, reference counting and full GC. It's also nice that Rust hasn't really decided on which way to go and have been doing some outstanding exploratory work on this field.
Oh yeah, and a good IRC channel with helpful people is a very valuable resource.
It's really nice to see buzz around new languages, that's the only way they will ever turn into mature production ready tools.
From there it is a simple matter to add memory, protection, devices, filesystems, networking, graphics etc :-)
You can return a pre-allocated buffer to be shared by all callers from a function in a safe way that prevents interference between callers. The callers only get to keep it for a limited time (region, actually), but usually that's no issue.
In exchange, the callers are gauranteed that the returned buffer contents can't be modified by any other part of the program while it is borrowed.
Here's some code for multiplying some polynomial vectors which uses this style, without allocating (except for a single initial allocation of the buffer):
https://github.com/scharris/WGFEM-Rust/blob/master/weak_grad...
Line 275: The structure holding the buffer and on which the operations are implemented.
Line 287: the buffer field
Line 297: The operation returning a structure (by value) which has an immutable "borrow" of the shared buffer. It's lifetime is tagged with lifetime 'a, tied to the lifetime of the implementing object.
Line 319: The buffer being included in the return value.
I really like the pattern. Get the efficiency of not allocating, kindof like old school libraries like LAPACK, without doing things like writing over your inputs :)
at the exit condition (whatever that is), exit the recursion and call something else with your new linked list.
(I used something similar to keep track of scope while recursively evaluating in this toy language I made once.)
Personally, I'm never a fan of punctuation first formatting styles - though I give some exception for dot (.) first method calls in fluent interfaces for C-family languages.
someInterface
.thatCan()
.chain()
.itsCalls();But yes recursion in a kernel is a "bad thing" unless you know the recursive bound.
Am I completely wrong here?
int malloc() {
return rand() % TOTAL_MEMORY;
} void* malloc(size_t size) {
static void* everything = 0;
void* result = everything;
everything += size;
return result;
}
When people are hacking together VMs for garbage collected languages, the first allocator often looks like this until they get around to actually collecting the garbage.It's just much less pernicious to do it that way, than my method, which means it's less fun :)
So it's just hard because writing a decent malloc/free is involved, algorithmically.
Yes.
> How much money would I need to stay in New York for 3 months?
About $1000/month for rent, give or take, and $112 for a metrocard. Plus whatever you spend on food (depends how much you cook) and entertainment.
I did then blow the money I'd saved by eating out every day, but that's a different matter.
I don't know why but I always assumed it was yet another "we teach non programmers some javascript"-programme. But it's really the opposite.
That's why kernel mode development is a lot more difficult and fun.
So, the suggestion looks like this:
struct freemem {
int len;
char *addr;
struct freemem *next;
};
// globals
int total_mem_size;
char *start_of_memory;
char *bump_pointer;
struct freemem *freelist;
Allocate is simply: if bump_pointer + MIN(size_to_allocate, sizeof(struct freemem)) < total_mem_size:
int *b = (int *) bump_pointer;
*b = MIN(size_to_allocate, sizeof(struct freemem);
return b + sizeof(int)
else
iterate over freelist, checking for size_to_allocate < freelist->len
return freelist->addr (the *b business is already taken care of)
Deallocate simply turns the discarded memory into a struct freemem and prepends it to the freelist, setting addr = the discarded address, and len = *(addr - sizeof(int)) (it's for this reason that we allocate a minimum size of sizeof(struct freemem))That's a basic malloc / free.
If you want a much longer explanation of this technique, I wrote a chapter about object pools[1] that discusses exactly this[2].
[1] http://gameprogrammingpatterns.com/object-pool.html
[2] http://gameprogrammingpatterns.com/object-pool.html#faster-particle-creation"I actually do have a malloc function because my Rust standard library needs to link against it"
He hasn't implemented malloc in his kernel yet (except for a stub).
I just want to point that out since women don't get enough cred in our field.
It doesn't lock you into the system allocator. You can LD_PRELOAD your own, and in the past Rust has even shipped with and used jemalloc[1] (though I believe it's not using it at the moment). Furthermore, as alluded to at the end of the article, you can override the compiler's malloc implementation via what Rust calls "lang items" (which are sadly ill-documented).
> He hasn't implemented malloc in his kernel yet (except for a stub).
Rather, Julia hasn't implemented malloc in her kernel yet.
I think you could forgive people for not knowing that Jason and Julia are the same person? (that's what you are implying, right?)
There's a support library for writing freestanding / embedded Rust called rust-core [1]. This library requires that you define a malloc function: it declares
extern { pub fn malloc(size: uint) -> *mut u8; }
However, it doesn't require that the malloc implementation be more than a stub, and as long as I don't allocate memory at any point that works fine.
pub unsafe fn putchar(x: u16, y: u16, c: u8) {
let idx : uint = (y * VGA_WIDTH * 2 + x * 2) as uint;
// 0xb8000 is the VGA buffer
*((0xb8000 + idx) as *mut u16) = make_vgaentry(c, Black, Yellow);
}Having all one's memory usage laid out at compile time really does provide some benefits. For one thing, it makes keeping track of who is wasting memory really easy! It is far too common to over allocate a buffer when initially developing code and to not tune it in later. Indeed, having to justify every allocated byte means developers will sit and do math and think about their code before writing it.
Odd as it may sound, this takes a fraction of the time that tracing down a memory leak does! Which is where the other huge savings comes in, no more memory leaks!
That of course leads right up to the other reason why heap-free programming is so common in embedded: Stability.
Memory leaks are a very common class of bug, for deeply embedded devices, it is not reasonable to have the user power cycle. Avoiding memory leaks removes one huge class of bugs. Add to that the lack of threads, and you knock out a second large class of bugs. In the end, one's test surface is greatly reduced!
That said, it is an interesting experience, I went from C# to embedded C# (yes it exists! .Net Micro Framework) which generally avoids allocations if possible, to straight embedded C and C++.
Some wise guy may now remark about smart pointers, to which I'll reply "my stack is 4k, go away!" Allocating on the stack can still fail with an OOM! Static allocation fails with an OOM at compile time! (Of course it is still possible to blow one's stack, but it is a lot harder when you aren't allocating structs or classes on it!)
Oh, and finally, a great interview question is to ask someone where in memory statics are stored. Heck just ask what are the types of memory allocation in C or C++. The percent of candidates that can pass this is quite low! Bonus points if the candidate knows that static consts may be stored in a separate read only region! Super extra bonus points if they list uninit'd and initialized data! That is very much OS and linker options dependent however, assuming one even has an OS!
80%+ of candidates will look at code such as
static int16_t value = 5;
and, when asked where it is stored at, will answer either heap or stack. (Not quite sure why people say stack.)Really, what you want is to look at a diagram like the one at http://www.geeksforgeeks.org/memory-layout-of-c-program/
And again, extra credit if they can talk about the various segments on their OS of choice. The above linked most certainly varies by platform, but anyone who has a true understanding of how one platform is laid out will be able to easily comprehend how other platforms go about things.
On a more practical note, all this came in handy when using C++ on a platform that didn't call constructors on statically declared classes! I opened up the debugger, looked at my pointer, say a bunch of 0's, then my class data, and realized I had no v-table! (I was crashing whenever I called any virtual functions.) Suffice to say, understanding what a v-table is, and how it worked, and how compilers created them, helped a ton. I then went off to ask the gentleman who wrote our run time why I had no v-table, well, turns out he hadn't written the code to go about and call constructors!
It is good to know that all of the "magic" that happens behind the scenes is not magic at all, but perfectly comprehensible code written by a developer just like you or I!
You are wise to implement malloc once...you are a fool (or a masochist) to implement it twice.