IO优化:使用mmap使读取文件的效率提升

在wps Android端的日子过得很快,我所在的组是IO组。因为WPS需要大量的和文档打交道,所以有专门的组负责IO。
最近想探索一下,是否可以把mmap接入到wps上。于是做了 “手写mmap进行读盘”的探索。

这里说一句,因为java 层的MappedByteBuffer 虽然也是采用了mmap。但是我发现有一个缺点是
没有提供unmap方法,很容易超过最大限制导致映射失败

我不知道为什么很多博客说MappedByteBuffer效率会高,如果你有demo可以证明这一点,可以在评论一起交流。

先说用JNI层的mmap后,相比缓冲流提升了多少效率。
这是读一个G文件的测试数据
IO优化:使用mmap使读取文件的效率提升
这里可以看到,大概提升了12%的效率。

后面我换了一台手机小米的Redmi K 30 5G 内存6G。
缓冲流:
第一个1.5G的文件,第一次读花了2641毫秒,第二次之后就直接变成470 毫秒左右。
所以应该是现在设备好了之后,自带的系统文件管理器做了很多优化,第一次才会发生直接IO,第二次以及以后,都是用缓存。

mmap:
读一个1.5G的文件,第一次花了3100毫秒,第二次之后就稳定在700毫秒左右

以上的第一次是在开机后的第一次。
总的来说:在一些低端设备上,mmap比缓冲流效率大概高了12%。而在较为高端的设备上,其实不建议采用mmap去实现文件的读取的,就算是大文件,还是建议用缓冲流即可。

所以我就有了另外的疑问:MMKV为什么会比Android自带的SharedPreferences快那么多。
官方文档里,MMKV主要做了两个优化,第一个是用mmap实现了文件的存取盘,第二个采用了protobuf协议。

虽然我还没有做频繁读写盘的测试,不知道频繁的读写盘 “缓冲流”和 “mmap” 的表现差异,但是这里就先猜测,protobuf协议才是性能提升的关键。

这是代码:


public class FileTestActivity extends Activity implements View.OnClickListener {

	String filePath = "/storage/emulated/0/test1";
	Button btn_mmap_read;
	Button btn_buffer_read;
	Button btn_map_read;

	@Override
	protected void onCreate(@Nullable Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_file_test);

		btn_mmap_read = findViewById(R.id.btn_mmap_read);
		btn_buffer_read = findViewById(R.id.btn_buffer_read);
		btn_map_read = findViewById(R.id.btn_map_read);

		btn_mmap_read.setOnClickListener(this);
		btn_buffer_read.setOnClickListener(this);
		btn_map_read.setOnClickListener(this);
	}

	@Override
	public void onClick(View v) {
		try {
			if (v == btn_mmap_read) {
				readMmap();
			} else if (v == btn_buffer_read) {
				readBuffer();
			} else if (v == btn_map_read) {
				readMap();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void readMap() throws Exception {
		long l = System.currentTimeMillis();
		FileInputStream fileInputStream = new FileInputStream(filePath);

		byte[] mmapBytes = new byte[1024 * 1024 * 10];

		FileChannel channel = fileInputStream.getChannel();
		MappedByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
		while (buf.hasRemaining()) {
			buf.get(mmapBytes);
		}

		channel.close();
		fileInputStream.close();
		long l1 = System.currentTimeMillis();
		System.out.println("readMap 执行时间:" + (l1 - l));
	}

	private void readBuffer() throws Exception {

		long l = System.currentTimeMillis();
		byte[] mmapBytes = new byte[65535];
		FileInputStream fileInputStream = new FileInputStream(filePath);
		BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
		while(bufferedInputStream.read(mmapBytes,0,mmapBytes.length) > 0) {
		}
		bufferedInputStream.close();
		long l1 = System.currentTimeMillis();
		System.out.println("readBuffer 执行时间:" + (l1 - l));
	}

	private void readMmap() throws IOException {

		long l = System.currentTimeMillis();

		byte[] mmapBytes = new byte[1024 * 1024 * 10];

		MmapInputStream mmapInputStream = new MmapInputStream(filePath);

		int fileSize = mmapInputStream.getFileSize(filePath);

		int tempLen = 0;
		int len = 0;
		while (len < fileSize) {
			tempLen = mmapBytes.length;
			if (len + tempLen >= fileSize) {
				tempLen = fileSize - len;
			}
			mmapInputStream.readByte(mmapBytes,len,tempLen);
			len += tempLen;
		}
		mmapInputStream.close();
		long l1 = System.currentTimeMillis();
		System.out.println("readMmap 执行时间:" + (l1 - l));
	}
}
public class MmapInputStream extends InputStream {

	static {
		System.loadLibrary("native-lib");
	}

	private String mFilePath;

	private int fileSize;

	public MmapInputStream(String filePath) {
		this.mFilePath = filePath;
		fileSize = getFileSize(filePath);
		mmapOpen(filePath,fileSize);
	}

	private byte[] cacheByte;
	private int position = -1;

	private final int len = 65535 * 100;
	private int readLen = 0;

	private void readCacheByte() {
		if (cacheByte == null || position >= cacheByte.length) {
			cacheByte = null;
			int tempLen = len;
			if (fileSize - readLen < tempLen) {
				tempLen = fileSize - readLen;
			}
			long l = System.currentTimeMillis();
			cacheByte = read(readLen, tempLen);
			long l1 = System.currentTimeMillis();
			System.out.println("readCacheByte:" + (l1 - l));
			position = 0;
		}
	}

	@Override
	public int available() throws IOException {
		return fileSize - readLen;
	}

	@Override
	public synchronized void mark(int readlimit) {
		super.mark(readlimit);
	}

	@Override
	public boolean markSupported() {
		return super.markSupported();
	}

	@Override
	public int read(byte[] b, int off, int len) throws IOException {
		if (readLen >= fileSize) {
			return -1;
		}
		readCacheByte();
		int temp = len;
		if (position + len >= cacheByte.length) {
			temp = cacheByte.length - position;
			Arrays.fill(b,temp,b.length, (byte) 0);
		}
		System.arraycopy(cacheByte,position,b,off,temp);
		readLen += temp;
		position += temp;
		return temp;
	}

	@Override
	public int read() throws IOException {

		if (readLen >= fileSize) {
			return -1;
		}
		readCacheByte();
		if (cacheByte == null || cacheByte.length == 0 || position >= cacheByte.length) {
			return -1;
		}
		readLen += 1;
		return cacheByte[position++] & 0xff;
	}

	@Override
	public long skip(long n) throws IOException {
		return super.skip(n);
	}

	/**
	 * 开启共享映射
	 * @param absolutePath
	 */
	public native void mmapOpen(String absolutePath,int mapLen);

	@Override
	public void close() throws IOException {
		super.close();
		mmapClose(fileSize);
	}

	/**
	 * 关闭共享映射
	 */
	public native void mmapClose(int fileSize);

	/**
	 * 得到文件的大小
	 */
	public native int getFileSize(String filePath);

	/**
	 * 写入数据
	 * @param content
	 */
	public native void mmapWrite(String content);

	public native byte[] read(int start,int len);

	public native char[] readChar(int start,int len);

	public native String readString(int start,int len);

	public native int readByte(byte[] bytes,int start,int len);
}

#include <jni.h>
#include <string>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <filesystem>

extern "C" {
    char *mmapPtr = NULL;
}

int fileSize(const char* filename)
{
    FILE *fp=fopen(filename,"r");
    if(!fp) return -1;
    fseek(fp,0L,SEEK_END);
    int size=ftell(fp);
    fclose(fp);

    return size;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_example_testjni_temp_MmapInputStream_mmapOpen(JNIEnv *env, jobject thiz, jstring path,
                                                  jint len) {

    const char *file_path = env->GetStringUTFChars(path, 0);

    // 文件不存在
    if (access(file_path, F_OK) == -1) {
        return;
    }

    int fd = open(file_path, O_RDWR | O_CREAT, 0644); //打开本地磁盘中的文件(如果没有就创建一个), 获取fd,0644是可读写的意思
    if (fd == -1) {
        perror("open error");
    }

    mmapPtr = (char *) mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    if (mmapPtr == MAP_FAILED) {
        perror("mmap error");
    }
    //关闭文件句柄
    close(fd);

}


extern "C"
JNIEXPORT void JNICALL
Java_com_example_testjni_temp_MmapInputStream_mmapClose(JNIEnv *env, jobject thiz,jint fileSize) {
    if (mmapPtr != NULL) {
        // 释放内存映射区
        int ret = munmap(mmapPtr, fileSize);
        if (ret == -1) {
            perror("munmap error");
        }
    }
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_testjni_temp_MmapInputStream_mmapWrite(JNIEnv *env, jobject thiz, jstring content) {
    if (mmapPtr != NULL) {
        const char *c_content = env->GetStringUTFChars(content, 0);
        // 修改映射区数据
        strcpy(mmapPtr, c_content);
    }
}


extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_example_testjni_temp_MmapInputStream_read(JNIEnv *env, jobject thiz, jint start, jint len) {

    auto *b = (jbyte *) mmapPtr; //转化
    b += start;
    jbyteArray myJByteArray = env->NewByteArray(len);
    env->SetByteArrayRegion(myJByteArray, 0, len, b);
    b -= start;
    return myJByteArray;
}


extern "C"
JNIEXPORT jint JNICALL
Java_com_example_testjni_temp_MmapInputStream_getFileSize(JNIEnv *env, jobject thiz, jstring file_path) {
    const char *filePath = env->GetStringUTFChars(file_path, 0);
    return fileSize(filePath);
}
extern "C"
JNIEXPORT jcharArray JNICALL
Java_com_example_testjni_temp_MmapInputStream_readChar(JNIEnv *env, jobject thiz, jint start, jint len) {

    auto *b = (jchar *) mmapPtr; //转化
    b += start;
    jcharArray myJByteArray = env->NewCharArray(len);
    env->SetCharArrayRegion(myJByteArray, 0, len, b);
    b -= start;
    return myJByteArray;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_testjni_temp_MmapInputStream_readString(JNIEnv *env, jobject thiz, jint start,
                                                    jint len) {

    int cLen = len;
    auto *b = mmapPtr;
//    char * p = new char[cLen + 1];
    char *p = (char *) malloc(len);

    memset(p, ' ', len);

    memcpy(p, b + start, len);

    auto *p1 = (const char *) p;

    jstring result = env->NewStringUTF(p1);

    delete p;

    return result;

}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_testjni_temp_MmapInputStream_readByte(JNIEnv *env, jobject thiz,
                                                       jbyteArray bytes,jint start,int len) {

    auto *b = (jbyte *) mmapPtr; //转化
    b += start;
    env->SetByteArrayRegion(bytes, 0, len, b);

    return 1;
}

因为wps在处理文件的时候,还涉及到转码的过程。转码的过程是用reader处理的,我把MmapInputStream传入reader后,发现效果其实和传入FileInputStream差不多多少,一个1G的文件大概可以快1秒钟。

wps处理的文档大都是50M以内,超过了本身进程的内存也不够了,所以这一次的优化探索就只是当作学习mmap的使用了。
在贴上我关于读盘的结论:

上一篇:bfs的练习


下一篇:mmap可以让程序员解锁哪些骚操作?