$ sbcl
This is SBCL 1.0.57.0.debian, an implementation of ANSI Common Lisp.
...
* (defun nlist (n) (loop for i from 1 to n collect i))
NLIST
* (nlist 5)
(1 2 3 4 5)
* (compile 'nlist)
NLIST
NIL
NIL
Let’s see how long it takes to allocate two million 500-item lists,
totaling a billion allocations: * (time (dotimes (i 2000000) (nlist 500)))
Evaluation took:
6.477 seconds of real time
6.460403 seconds of total run time (6.416401 user, 0.044002 system)
[ Run times consist of 0.384 seconds GC time, and 6.077 seconds non-GC time. ]
99.74% CPU
18,093,926,186 processor cycles
16,032,024,704 bytes consed
NIL
That’s 6.5 nanoseconds and 18 “processor cycles” (does SBCL use
performance counters for this or is it guessing?) per 16-byte
allocation. That’s about 150 million allocations per second,
including the time to initialize those allocations and increment the
loop counter and whatnot, and also (contra chrisseaton) the time to deallocate those lists. If we increase the list length and
proportionally decrease the iteration count, this performance remains
consistent up to 50,000-item lists, but at 2000 iterations of half a
million items, it starts taking 10 seconds instead, presumably because
the lists no longer fit into the generational garbage collector’s
nursery, so it has to spend a third of its time in the garbage
collector.The above is running on one core of a “Intel(R) Core(TM) i7-3840QM CPU @ 2.80GHz”, so if 18 processor cycles is not correct, it’s a damned good guess. Also, this CPU is from 02012, nine years ago.
What does this look like at the machine level? NLIST disassembles to 87 lines of assembly, so I’ll spare you most of it and excerpt only one of the allocations:
* (disassemble 'nlist)
; disassembly for NLIST
; 029ECE7D: 488B4DF8 MOV RCX, [RBP-8] ; no-arg-parsing entry point
...
; EF9: 4D8B5C2418 MOV R11, [R12+24]
; EFE: 498D4B10 LEA RCX, [R11+16]
; F02: 49394C2420 CMP [R12+32], RCX
; F07: 0F8696000000 JBE L9
; F0D: 49894C2418 MOV [R12+24], RCX
; F12: 498D4B07 LEA RCX, [R11+7]
; F16: L4: 49316C2440 XOR [R12+64], RBP
...
; FA3: L9: 6A10 PUSH 16
; FA5: 4C8D1C2570724200 LEA R11, [#x427270] ; alloc_tramp
; FAD: 41FFD3 CALL R11
; FB0: 59 POP RCX
; FB1: 488D4907 LEA RCX, [RCX+7]
; FB5: E95CFFFFFF JMP L4
As best I’ve been able to figure out, the nursery allocation pointer
is stored in memory at [R12+24], the pointer to the freshly allocated
dotted pair comes out of this sequence (at L4) in RCX, and the pointer
to the end of the nursery is stored in memory at [R12+32]. So the
actual allocation is, in the normal case, the six instructions from
029ECEF9 up to 029ECF16. If the nursery is full, it takes the jump to
L9 to invoke a minor garbage collection. It may help to know that on
amd64 SBCL represents dotted-pair pointers with, essentially, a
pointer to their 7th byte, which is what all that RCX+7 nonsense is
about.In games it's common to use a similar pointer-bumping allocator for many allocations and then deallocate the whole heap at the end of the frame (by resetting the allocation pointer)—in that case, you don't even need the check against the heap limit, you just need to make sure you never come close to overflowing it. The Packrat-parsing backend of the project I'm working on at the moment, http://gitlab.special-circumstanc.es/hammer/hammer, does the same thing, but the arena is per-parse rather than per-frame, and it may be divided into multiple separately malloced blocks. Also, unlike SBCL, Hammer is in C, so it can't inline the calls to `h_arena_alloc`; they typically have to pay two instructions of argument setup, one instruction of function call, one instruction of return value handling, another instruction of PLT shared library overhead, and then the actual function is 26 instructions in the usual case.
The moral of the story is that the performance of fundamental operations depends strongly on the tradeoffs you make in your system design.
It's still true, though, that pointer-heavy data structures are terrible for cache locality (an L3 cache miss costs about 100 ns, as much as 15 allocations on one core, but typically the L3 cache is shared across all cores or at least all the cores on one socket), and that HPC consists mostly of large numerical arrays and not pointer chasing. Consequently, the software stacks used in HPC aren't optimized to make allocation cheap; they're optimized for other operations, and so they allow allocation to be expensive. I've explored this fascinating issue in somewhat more detail in http://canonical.org/~kragen/memory-models.
(It's a deeply damning commentary on the climate of boastful intellectual vacuity this site fosters that comments like this get downvoted for showing empirical evidence and comparing results from a wide variety of contexts, while confident but totally clueless comments about how an allocation necessarily involves acquiring locks and whatnot get voted up to the top. I guess they take less time to make!)