Cold start optimization based on binary rearrangement is very popular recently, which involves mMAP knowledge. I have long wanted to study MMAP systematically. Just now, the recent project plans to develop a set of APM monitoring, which requires frequent file writing operations when recording relevant data. I wondered if I could use Mmap for high-performance file reads and writes. So I systematically studied the knowledge related to MMAP. After a look, I found that although MMAP has many advantages, it is not as perfect as expected. This article will briefly explain what MMap is, how it works, how to use it, and when to use it.

Introduction to MMAP

Mmap is a method of memory-mapping files. A file or other object is mapped to the memory address space of a process to achieve a one-to-one mapping between the file disk address and a virtual address in the virtual address space of a process. After this mapping is implemented, the process can read and write the memory in the way of Pointers, and the system will automatically write back dirty pages to the corresponding file disk. Operations on related files do not need to call read,write and other system call functions. Conversely, changes made by the kernel space to this area are directly reflected in the user space, enabling file sharing between different processes. As shown below:

Let’s look at the mmap file as an example:

- (void)testRead
{
    NSString *filePath = [@"~/Library/testFile" stringByExpandingTildeInPath];
    int i = 0;
    int fd = open(filePath.UTF8String, O_RDWR);
    void *m_ptr = NULL;
    // Map a 1GB large file
    m_ptr = mmap(m_ptr, k1Gb, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    // Prepare 1KB buffer for each 1KB read
    uint8_t bytes[k1Kb];
    int left = 0;
    int right = k1Mb-k1Kb;
    while (i < k1Mb) {
        size_t offSet = (k1Kb * i);
        // Copy 1KB of content from the file into our buff
        memcpy(bytes, m_ptr + offSet, k1Kb);
        //do something with 1kb buffer data ...i++; }}Copy the code

In the example above, we map a 1GB file, read 1KB of it at a time, and then do something with the read data.

The mmap function returns a pointer of type void *, which we can consider as a data array. It is exactly the same as the array allocated in memory by malloc function. It can also be manipulated by memcpy and other memory manipulation functions.

You may have noticed that the above code maps 1 gigabyte of files into memory. On a memory-constrained system like iOS, will this cause our App memory usage to be too high and be forcibly killed by the system? The answer is no. Because Mmap only maps files to memory, it does not load the entire contents of a file into physical memory at once. If you use XCode to look at the memory usage of your App at this point, you’ll see that loading a 1GB file through Mmap doesn’t take up much memory (in fact, it might only take up kilobytes of memory, due to the virtual memory page loading mechanism, which we’ll cover in more detail later).

The basic principle of

In the introduction to the basics of MMAP, we mentioned that Mmap can load large files into memory without consuming too much physical memory. How does Mmap do this? Let’s explore his rationale.

Virtual memory

In the previous article, we mentioned the concept of virtual memory, which is the basis of MMap. What is virtual memory?

Virtual memory is defined as:

Virtual memory is a technology of memory management in computer system. It makes an application think that it has contiguous available memory (a contiguous complete address space), when in reality it is usually divided into multiple physical memory fragments, with portions temporarily stored on external disk storage for data exchange as needed. At present, most operating systems use virtual memory, such as “virtual memory” in the Windows family; Linux swap space, etc.

We are familiar with iOS development language to describe:

The system creates a set of contiguous memory addresses for each of our iOS apps (processes), but these memory addresses are not real physical memory addresses. When we access these addresses, the system translates them into the address of the corresponding physical memory (or other storage location). So we don’t have to worry about how these memory addresses are stored when we’re developing an App, we can just use them. The system will help us to read and write what we need in real physical storage.

Mmap maps memory

Based on virtual memory technology, in the case of MMAP, the system actually helps us to virtual a set of contiguous memory addresses, and the memory corresponding to the actual storage is a file on disk. When we access this part of memory and need to load the actual content, the system will load the memory page through the missing page interrupt. So, we load a 1GB file without using any physical memory, so we don’t use much memory in terms of memory usage

Mmap reads and writes memory

We return a void * memory address by calling the mmap function. This part of memory is the virtual memory address mapped by Mmap. We can read and write to this part of memory. The following describes how the system loads file content and writes content to a file respectively for read and write operations.

– read:

For a read operation, the system first checks whether the current read has been loaded into physical memory from the file. If already loaded, the contents of physical memory are returned directly. If it is not loaded, a page-missing interrupt is triggered, the system reads the file in units of memory page size (performing file IO operations), and the memory page is cached. To reduce the total NUMBER of I/O operations.

Write:

For write operations, the system marks the memory pages to which the content is written as dirty and writes the dirty pages in batches to the mapped file (IO) when appropriate.

Note: Modified dirty pages are not immediately updated back into the file, but there is a delay and you can call msync() to force the synchronization.

The advantages and disadvantages

Now that we know the basics and principles of MMAP, let’s look at its pros and cons:

advantages

  • The ability to load large files into memory with little or no footprint on physical memory (especially useful for mobile development scenarios where memory is scarce)
  • Files can be directly copied to user space data, compared to the conventionalread/writeOperations omit some additional steps (kernel-mode, multiple copies of user-mode content, page caching, etc., aboutread/wirteSee further reading for a brief introduction.)
  • Can be achieved bymmapThe sharing mechanism performs interprocess sharing

disadvantages

  • mmapThe API is relatively obscure
    • mmapRelated functions are low-level C functions and return Pointers. Subsequent direct operation of Pointers is error-prone. The file operation OC has a wrapperNSFileHandle, simple to use. (If only read,NSDataAlso supportmmapIn the way of referenceNSDataReadingOptionstheNSDataReadingMappedIfSafe)
    • Because it operates directly on memory, the Mmap function doesn’t have much to say about inconsistency between file size and mapped memory size, and operating on the wrong memory can trigger a memory access crash (see details).
    • High API difficulty leads to high implementation complexity, application and production environments need to be fully tested
  • If there are a lot of file updates, mmap’s advantage of avoiding two-state copies will be amortized, resulting in a lot of dirty page writes and the resulting random I/OS, so mMAP is not necessarily more efficient than normal writes with buffers for a lot of random writes.

Using the tutorial

With that said, let’s take a look at how the MMAP API is used.

The mapping file

To use the Mmap function to map files, you need to open an existing file using the C function open. Mmap needs to use the file handle obtained by open.

Void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset)

The meanings of the parameters are as follows:

  • start: the start address of the mapping area (from which memory is mapped), if passedNULL, the system will automatically select an appropriate address to start mapping (usually passed inNULL)
  • length: Mapping length
  • prot: indicates the mapping memory protection modeopenFile opening mode conflict, can pass|The operator specifies multiple types. It is usually selected in iOSPROT_READ,PROT_WRITE
  • flags: Mapping sharing mode with other processes, commonly used in iOSMAP_SHARED
  • fdUse:openFunction to specify which file to operate on
  • offset: Where in the file to start mapping

Returns a pointer of type void *, which is the memory address mapped successfully. If NULL is returned, the mapping fails. You can obtain the corresponding error code through the errno macro.

A simple code example would be:

- (void)testRead
{
    NSString *filePath = [@"~/Library/testFile" stringByExpandingTildeInPath];
    int fd = open(filePath.UTF8String, O_RDWR);
    void *m_ptr = NULL;
    NSUInteger k1Gb = 1024 * 1024 * 1024;
    m_ptr = mmap(m_ptr, k1Gb, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd); // The file can be closed after mapping is complete

    //read
    uint8_t buffer[1024];
    memcpy(buffer, m_ptr, 1024);
    //do something...
    
    //write
    *(uint8_t *)m_prt = 10;
}
Copy the code

After the mapping is successful, the returned pointer can be read and written normally, just as it was allocated in memory.

Remove the mapping

The munmap function is used to remove the memory of mapped files. The function is declared as int munmap(void * addr, size_t len).

  • addrUse:mmapObtained the mapped memory address
  • len: Mapping length

The return value is int, 0 for success and -1 for failure. Use errno to obtain the corresponding failure information.

Synchronizing Disk Data

Typically, changes to mapped memory are not written synchronously to disk files, but the system determines when it is appropriate to write. Use the msync function to synchronize changes to disk immediately. Int msync(void *addr, size_t len, int flags);

  • addr,lenAnd:mmap,munmapSimilarly, respectively for the mapped memory address and the specified length
  • flags: optionalMS_ASYNC,MS_SYNC,MS_INVALIDATE, using flagsMS_ASYNCThe function plans a synchronization, but returns immediately,

! The details

  • mmapThe function takes two argumentslengthandoffsetThe recommended size in the documentation is an integer multiple of the size of the memory page (data alignment). If it is not an integer multiple, iOS will automatically extend it to an appropriate integer multiple and fill in all the extra space0. The reason for this is that the minimum granularity of memory is pages, and the mapping of process virtual address space and memory is also in pages. To match memory operations, mMap’s mapping from disk to virtual address space must also be pages
  • Once the mapping is complete, the file can be closed. Because the mapping is to the actual disk address of the file, not the file itself, the file handle can be closed
  • Specified when mappinglengthCan be inconsistent with the file size. If the file is larger than the file, and the mapped file is expanded after the mapping, we can legally access the bytes that are within the current file size and within the memory mapping area. That is, if the file size keeps growing, the process can legally obtain any data within the mapping range, regardless of the size of the file at the time the mapping was created. To elaborate:
    • Premise: we assume that on 64-bit iOS13, a memory page size is 16k. (memory alignment has been tested in iOS13 and 12, they are slightly different, let’s use 13 as an example.)
    • There are three special cases:
      1. File length equal to map length, but not memory page alignment:Assuming a file size of 10,000 bytes, we map the size of 10,000 bytes (i.elengthThe argument is passed 10000). In this case, 10000 bytes is less than the size of a memory page (16K). Therefore, the mapping system automatically aligns memory and maps 16K memory. The system will automatically fill in 0 for some bytes between 10000 and 16384. This part of memory is accessed without error, but changes are not synchronized to the file.
      2. The file length is less than the mapping length:Assume the file size is 10000 bytes and the mapping size is 32K. At this time, because 10000 bytes is less than 1 memory page, the system will automatically map 16K memory and file associationCase 1Same, but since we mapped 32K memory, if we access the additional 16K memory, it will causeEXC_BAD_ACCESScollapse
      3. The file length is changing:
      • Variable length: Assuming file length is 0, we map 32K size, if this time direct access, because there is no corresponding physical file, it will triggerEXC_BAD_ACCESSTo collapse. But if we pass before accessing memoryftruncateAnd other means to expand the file, then access the file size range of parts can be synchronized to the file
      • Shorten: If the file length is 32K, we map 32K, when accessing 10000 bytes, we also shorten the file to 10000 bytes, and then continue accessing. At this point, the access within the 16K memory alignment range can still not crash, the system will automatically fill in all zeros when reading, write invalid. If access continues in the range of 16-32KEXC_BAD_ACCESSTo collapse.
    • conclusion: whenmmapWhen the length of the map is inconsistent with the file size, accessing portions that are out of the file size and in memory alignment will not crash, but all reads are zero and writes are invalid. Accessing portions that are out of file size and out of memory alignment causes memory access to crash.

Performance analysis

The performance of MMAP is analyzed in terms of memory and read/write time. The phone tested was iPhoneX, and the operating system was iOS13.3.1.

Memory footprint

As mentioned earlier, one advantage of Mmap is that it doesn’t take up much physical memory. Let’s look at the memory usage of mmap by reading a large file of 1 GB.

  • Read:

We use the following test code to test the read memory footprint:

- (void)testRead
{
    NSString *filePath = [@"~/Library/testFile" stringByExpandingTildeInPath];
    int i = 0;
    int fd = open(filePath.UTF8String, O_RDWR);
    ftruncate(fd, k1Gb);  // Change the size to 1Gb
    void *m_ptr = mmap(NULL, k1Gb, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    uint8_t bytes[k1Kb]; // A 1KB buffer
    while (i < k1Mb) {
        size_t offSet = (k1Kb * i);
        // Read 1KB into buffer at a timememcpy(bytes, m_ptr + offSet, k1Kb); i++; }}Copy the code

Running the App takes up 2.45MB of memory, and running the testRead function 2 seconds later takes up 2.51MB of memory, which only increases by less than 1K.

  • Write:

Reverse the source and destination addresses of memcpy in the above test code while loop: memcpy(m_ptr + offSet, bytes, k1Kb); Perform write tests. The result is 2.55MB memory for running App, 2.63MB memory for running test function 2 seconds later, and less than 1K memory growth.

As you can see from the above tests, mmap requires very little memory for both reading and writing.

Reading and writing time consuming

Another performance to watch is speed. Speaking of speed, since Mmap is also a way of reading and writing files, we usually contrast it with read and write, the regular way of reading and writing files. Here’s a quick look at how MMap differs from regular Read and Wirte.

Common file operations such as read and Wirte use page caching to improve read and write efficiency and protect disks. As a result, file pages need to be copied from the disk to the page cache before reading files. Because the page cache is in the kernel-state space and cannot be directly addressed by user processes, data pages in the page cache need to be copied to the corresponding user space in memory. In this way, after two data copy processes, the process can complete the task of obtaining the file content. The same is true for write operations. The buffer to be written cannot be directly accessed in the kernel space. It must be copied to the corresponding main memory of the kernel space and then written back to disk (delayed write back), which also requires two data copies. With mmap, there is no file copy operation for creating a new virtual memory region and creating the file disk address and virtual memory region mapping. When the data is accessed later and no data is found in the memory, the page missing exception process can be initiated. Through the established mapping relationship, data can be copied only once from the disk into the user space of the memory for the process to use.

From the above description, it seems that Mmap should be more efficient than normal file read and write operations. And there are articles on the web that say mMap can be very efficient. So is this really the case? Let’s compare mMAP and Read /write speed by reading/writing the same file.

  • Read: We are rightread,mmapRead tests were carried out respectively. For 1Gb files, 1KB of data was read each time and copied to buffer, and the sequential reads from beginning to end and random reads from beginning to end were recorded. The test results were as follows:
function operation Time consuming
read Order to read 276ms
mmap Order to read 366ms
read Random reads 549ms
mmap Random reads 416ms

It can be seen that regular READ is more efficient than MMAP in sequential reading. For random reads, mMAP has the advantage.

  • Write: We are rightwrite,mmapEach time 1KB data is written to the file, a total of 1G data is written to the file. The sequential write from beginning to end and random write from beginning to end are recorded. The test results are as follows:
function operation Time consuming
write Order to write 8.95 s
mmap Order to write 4.26 s
write The random write 20.4 s
mmap The random write 17.6 s

Mmap is faster than WRITE, both randomly and sequentially.

So we can see that it is basically true that MMAP is more efficient, except that in the case of reads, MMAP is better at reading random positions, while sequential reads are still faster.

Application scenarios

After analyzing the basic principles and performance of Mmap, let’s take a look at the scenarios in which Mmap should be used for iOS development

  1. mmapA big advantage is that mapped files take up no physical memory space and can therefore be used to read large files
  2. For read and write efficiency and conventionalread/writeComparison, suitable for scenarios where random reads and writes are required, and scenarios where files are written
  3. When a file needs to be held for a long time, or a file needs to be shared with other processes, and a large amount of data needs to be transferred for inter-process communication

If we have to choose between mmap and regular file reading and writing, to quote the conclusion of a certain god on Stack Overflow:

Use memory maps if you access data randomly, keep it around for a long time, or if you know you can share it with other processes. Read files normally if you access data sequentially or discard it after reading. And if either method makes your program less complex, do that. For many real world cases there’s no sure way to show one is faster without testing your actual application and NOT a benchmark.

My own opinion: if you don’t really need it or there is a huge performance improvement, stick with regular files. If you are going to use MMAP, do plenty of testing, or you may have unexpected problems.

Ultimate application: Display images without taking up memory

IOS is memory constrained, and images are huge memory hogs. Especially in the input method, Today and other extensions, the system limits the maximum amount of memory that can be applied for. So, if there are too many images or large images to display, it is very likely to crash our program.

Since the image rendering needs to decode the image into bitmap first, the system will hold the bitmap after the image display, resulting in memory occupation. There is a simple formula to calculate the bitmap memory size required for image display: width * height * 4, for example, a 100×100 width and height image, the bitmap data required after display will occupy 39K. If the image is bigger, it takes up more memory. This is independent of the image file compression format (PNG, JPG, etc.).

Now that we have learned MMAP, can we use Mmap to map a section of file memory and let the system store the bitmap in this part of the mapped memory? The answer is yes! And it doesn’t take up physical memory.

So how do we do that? I’m keeping you in suspense, but I’ll cover it in detail in my next article.

Knowledge extension:

In the process of exploring MMAP, there are many related knowledge points, which will help to better understand MMAP after clear research. We should not only learn one point of knowledge, but broaden the whole area of relevant knowledge. I have listed the knowledge points and reference materials related to MMAP to facilitate the extension of knowledge.

Virtual memory

  • Concept of virtual memory, refer
    • zhuanlan.zhihu.com/p/96098896
  • The concept of memory pages
  • Page table concept, multi-level page table
  • LRU elimination algorithm, refer to:
    • Blog.csdn.net/hopeztm/art…
    • Blog.csdn.net/elricboa/ar…
    • www.cnblogs.com/Dhouse/p/86…

read/write

  • Use the API
  • Principle, reference
    • Blog.csdn.net/u013837209/…

Kernel mode, user mode

Both MMAP and Read /write involve system calls and transitions between kernel and user states. The following articles will help you understand user and kernel states:

  • Why multiple states:
    • www.cnblogs.com/objectc/p/4…
  • How to switch/trap instructions principle:
    • www.cnblogs.com/maxigang/p/…
    • Blog.csdn.net/shanghx_123…
    • Wenku.baidu.com/view/a7bbf3…
    • Kernel stack: blog.csdn.net/qq_41727218…