Multiple MemoryMappedAudioFormatReader for single file


#1

Hi everybody,

I am using multiple (like, 900+) MemoryMappedAudioFormatReaders for accessing different ranges of an audio file. On Windows everything works out OK, but on OS X the destructors of the MemoryMappedFile in each reader spends huge amounts of time in the close() method, which is a direct OS call to the mmap C library.

Is it bad practice to use multiple memory maps with different regions for one file or is the performance so bad because I unmap the file too often (one call to close() would be enough)? I could rewrite the system so that not every memory mapped reader has a MemoryMappedFile member which it deletes (which is the default implementation of the MemoryMappedAudioFormatReader), but rather share one MemoryMappedFile object that is ref-counted and deleted after the last reader stopped using it.


#2

I don’t really know why the close would take so long. Is this really OS X specific? Have you tried it on Linux (which also uses mmap)?

That would be super nice!!


#3

Yeah, it makes little sense to ask the OS do any more than necessary.

If the OS is 64-bit then you might as well just map each file once with one big shared memory map. There’s no advantage in mapping sub-regions separately.

The only reason to use sub-regions is when your target OS is 32-bit and doesn’t have enough address-space to map everything you might use. Then, you should use a smarter approach and coalesce all your active regions into a minimal set of regions, which you then map.

(That’s how we do the audio playback in Tracktion 64/32 bit versions)


#4

Unfortunately I don’t have a linux system running, so I can’t check it there. But on Windows the problem does not occur and using the MemoryMappedAudioFormatReaders for AIFF and WAV also don’t show this behavior, although I am mapping multiple audio files with them, not one big monolithic chunk (but I’ll test it with a big audio file).

Interesting, so having one large file (~1 GB) and mapping it as one big mmap (let’s ignore 32bit problems for now) performs better than using multiple mmaps on the same file?

This contradicts the official Apple guidelines for handling memory mapped files:

When randomly accessing a very large file, it’s often a better idea to map only a small portion of the file at a time. The problem with mapping large files is that the file consumes active memory. If the file is large enough, the system might be forced to page out other portions of memory to load your file. Mapping more than one file into memory just compounds this problem.

(taken from [here] (https://developer.apple.com/library/mac/documentation/FileManagement/Conceptual/FileSystemAdvancedPT/MappingFilesIntoMemory/MappingFilesIntoMemory.html))

I tried to create small memory maps just in time (shortly before read access and closed it after the file was finished playing), but that yielded the same poor performance (even worse because the destructor blocked the streaming threads).


#5

Well, if you only needed a single subsection then yes, it’d be silly to map the whole thing, but if you say you need 900 subsections then it would probably be best to just map it all. The OS is pretty smart about which bits to cache.


#6

Alright, makes sense.

BTW is there any performance advantage by using the MAP_PRIVATE flag in the mmap call? If I understand it correctly, the MAP_SHARED flag is only needed when writing from different processes to the same file but for reading it shouldn’t be necessary…


#7

No idea whether that flag affects performance, but seems reasonable to assume that being shared could only make performance worse, not better. So best avoided unless you really need it.


#8

Well, it’s the default flag in the MemoryMappedFile constructor:

void* m = mmap (0, (size_t) range.getLength(),
                    mode == readWrite ? (PROT_READ | PROT_WRITE) : PROT_READ,
                    MAP_SHARED, fileHandle,
                    (off_t) range.getStart());

(from juce_posix_SharedCode.h)

I wouldn’t hack around in the JUCE internals without a real reason, but maybe you could choose the MAP_PRIVATE flag if the MemoryMappedFile is opened in read mode:

..., mode == readOnly ? MAP_PRIVATE : MAP_SHARED, fileHandle, ...

Anyways, changing the design to one memory mapped file solves this issue (the destructor of this file is still pretty slow with ~4 ms, but at least that does not scale up). Thanks for the clarifications…


#9

I don’t think that’s necessarily the best fix. Apps may open a file in read mode while another app opens the same file (or socket) in write mode. The app can then use this file to send data to the other app. I think we should probably add another flag.


#10

Yeah you’re right. Actually it does not make anything faster, according to the nice folks from stack overflow:


#11

OK I’ve added an exclusive flag to the MemoryMappedFile (see latest develop). You can try this, maybe it improves performance.


#12

Alright, I’ll try it, thanks.