Live data from Hacker News

Everything old is new again: memory optimization

nibblestew.blogspot.com

81–90 of 168 posts

Re: Everything old is new again: memory optimization

#82
post #71

Earlier quoted context omitted.

Incrementing or decrementing a shared counter is done with an atomic instruction, not with a locked critical section. This has negligible overhead in most cases. For instance, if the shared counter is already in some cache memory the overhead is smaller than a normal non-atomic access to the main memory. The intrinsic overhead of an atomic instruction is typically about the same as that of a simple memory access to d…

Atomic operations, especially RMW operations are very expensive, though. Not as expensive as a syscall, of course, but still a lot more expensive than non-atomic ones. Exactly because they break things like caches

Not only that, they write back to main memory. There's limited bandwidth between the CPU and main memory and with multithreading you are looking at pretty significantly increasing the amount of data transferred between the CPU and memory.

This is such a problem that the JVM gives threads their own allocation pools to write to before flushing back to the main heap. All to reduce the number of atomic writes to the pointer tracking memory in the heap.

Re: Everything old is new again: memory optimization

#83
post #43

A lot of frameworks that use variants of "mark and sweep" garbage collection instead of automatic reference counting are built with the assumption that RAM is cheap and CPU cycles aren't, so they are highly optimized CPU-wise, but otherwise are RAM inefficient. I wonder if frameworks like dotnet or JVM will introduce reference counting as a way to lower the RAM footprint?

That's not strictly true. Mark and sweep is tunable in ways ARC is not. You can increase frequency, reducing memory at the cost of increased compute, for example.

M&S also doesn't necessitate having a moving and compacting GC. That's the thing that actually makes the JVM's heap greedy.

Go also does M&S and yet uses less memory. Why? Because go isn't compacting, it's instead calling malloc and free based on the results of each GC. This means that go has slower allocation and a bigger risk of memory fragmentation, but also it keeps the go memory usage reduced compared to the JVM.

Re: Everything old is new again: memory optimization

#84
> This sounds like a job for Python. Indeed, an implementation takes fewer than 30 lines of code.

I don't know if the implementation is written in a "low-level" way to be more accessible to users of other programming languages, but it can certainly be done more simply leveraging the standard library:

  from collections import Counter
  import sys

  with open(sys.argv[1]) as f:
      words = Counter(word for line in f for word in line.split())

  for word, count in words.most_common():
      print(count, word)
At the very least, manually creating a (count, word) list from the dict items and then sorting and reversing it in-place is ignoring common idioms. `sorted` creates a copy already, and it can be passed a sort key and an option to sort in reverse order. A pure dict version could be:

  import sys

  with open(sys.argv[1]) as f:
    counts = {}
    for line in f:
      for word in line.split():
        counts[word] = counts.get(word, 0) + 1

  stats = sorted(counts.items(), key=lambda item: item[1], reverse=True)

  for word, count in stats:
      print(count, word)
(No, of course none of this is going to improve memory consumption meaningfully; maybe it's even worse, although intuitively I expect it to make very little difference either way. But I really feel like if you're going to pay the price for Python, you should get this kind of convenience out of it.)

Anyway, none of this is exactly revelatory. I was hoping we'd see some deeper investigation of what is actually being allocated. (Although I guess really the author's goal is to promote this Pystd project. It does look pretty neat.)

Re: Everything old is new again: memory optimization

#85
post #49

Earlier quoted context omitted.

https://learn.microsoft.com/en-us/sysinternals/downloads/vmm... for an empty sublime text window gives me: - 100MB 'image' (ie executable code; the executable itself plus all the OS libraries loaded.) - 40MB heap - 50MB "mapped file", mostly fonts opened with mmap() or the windows equivalent - 45MB stack (each thread gets 2MB) - 40MB "shareable" (no idea) - 5MB "unusable" (appears to be address space that's not usabl…

But I have sublime text open with a hundred files and it's using 12mb.

And how does that breakdown in vmmap? I'm guessing that's working set vs. the whole virtual memory allocation (which is definitely always an overestimate and not the same as RAM)

Re: Everything old is new again: memory optimization

#86
post #29
post #6

Well, we can use memoryview for the dict generation avoiding creation of string objects until the time for the output: import re, operator def count_words(filename): with open(filename, 'rb') as fp: data= memoryview(fp.read()) word_counts= {} for match in re.finditer(br'\S+', data): word= data[match.start(): match.end()] try: word_counts[word]+= 1 except KeyError: word_counts[word]= 1 word_counts= sorted(word_counts.…

This doesn't do the same thing though, since it's not Unicode aware. >>> 'x\u2009 a'.split() ['x', 'a'] # incorrect; in bytes mode, `\S` doesn't know about unicode whitespace >>> list(re.finditer(br'\S+', 'x\u2009 a'.encode())) [ , ] # correct, in unicode mode >>> list(re.finditer(r'\S+', 'x\u2009 a')) [ , ]

OP's .split_ascii() doesn't handle U+2009 as well.

edit: OP's fully native C++ version using Pystd

Re: Everything old is new again: memory optimization

#88
post #52

Earlier quoted context omitted.

Reference counting in multithreaded systems is much more expensive than it sounds because of the synchronization overhead. I don't see it coming back. I don't think it saves massive amounts of memory, either, especially given my observation with vmmap upthread that in many cases the code itself is a dominant part of the (virtual) memory usage.

If you use an ownership/lifetime system under the hood you only pay that synchronization overhead when ownership truly changes, i.e. when a reference is added or removed that might actually impact the object's lifecycle. That's a rare case with most uses of reference counting; most of the time you're creating a "sub"-reference and its lifetime is strictly bounded by some existing owning reference.

There are 2 unavoidable atomic updates for RC, the allocation and the free event. That alone will significantly increase the amount of traffic per thread back to main memory.

A lifetime system could possibly eliminate those, but it'd be hard to add to the JVM at this point. The JVM sort of has it in terms of escape analysis, but that's notoriously easy to defeat with pretty typical java code.

Re: Everything old is new again: memory optimization

#89

I'm always confused as hell how little insight we have in memory consumption. I look at memory profiles of rnomal apps and often think "what is burning that memory". Modern compression works so well, whats happening? Open your taskmaster and look through apps and you might ask yourself this. For example (lets ignore chrome, ms teams and all the other bloat) sublime consumes 200mb. I have 4 text files open. What is it…

> I look at memory profiles of rnomal apps and often think "what is burning that memory". As a corrolary to this: I look at CPU utilization graphs. Programs are completely idle. "What is burning all that CPU?!" I remember using a computer with RAM measured in two-digit amounts of MiB. CPU measured in low hundreds of MHz. It felt just as fast -- sometimes faster -- as modern computers. Where is all of that extra RAM b…

> I remember using a computer with RAM measured in two-digit amounts of MiB

Yes, so do I. It was limited to 800x600x16 color mode or 320x200x256. A significant amount of memory gets consumed by graphical assets, especially in web browsers which tend to keep uncompressed copies of images around so they can blit them into position.

But a lot is wasted, often by routing things through single bottlenecks in the whole system. Antivirus programs. Global locks. Syncing to the filesystem at the wrong granularity. And so on.

Re: Everything old is new again: memory optimization

#90

Earlier quoted context omitted.

Retrofitting new patterns or ideas is underutilized only when it is not worth the change. string_view example is trivial and anyone who cared enough about the extra allocations that could have happened already (no copy-elision taking place) rolled their own version of string_view or simply used char+len pattern. Those folks do not wait for the new standard to come along when they can already have the solution now. st…

Existing APIs for file IO in STL don't return string views into the file buffer of the library (when using buffered IO). That is something you could do, as an example. Optional being opinionated I don't think I agree with. It is better to have an optional of something that can't be null (such as a reference) than have everything be implicitly nullable (such as raw pointers). This means you have to care about the null…

Niche optimizations are trivial to automate in modern C++ if you wish. Many code bases automagically generate them.

The caveat is that niche optimizations are not perfectly portable, they can have edge cases. Strict portability is likely why the C++ standard makes niche optimization optional.

Post reply on HN