文章目录
1. 引言
在嵌入式系统中,数据处理效率对系统性能至关重要。在 QNX 系统中,通过 mmap
函数将文件映射到内存后,可以选择启用或禁用缓存(Cache)。本文将对比在缓存(Cache)和非缓存(NoCache)情况下,使用 memcpy
和汇编版本 asm_memcpy
的数据传输速度,帮助开发者优化系统性能。
2. 为什么要做 Cache 与 NoCache 对比?
缓存(Cache)可以显著提升系统性能,因为它允许 CPU 以更快的速度访问频繁使用的数据。然而,在某些场景下,如实时系统或对数据一致性要求较高的应用,使用缓存可能引发数据同步问题或增加不必要的延迟。因此,在特定情况下,禁用缓存(NoCache)可能更为合适。
3. mmap nocache 简介
在 QNX 系统中,mmap
函数可以将文件或设备内存映射到进程的地址空间。通过使用 MAP_NOCACHE
标志,映射的内存页不会被缓存到文件系统中,而是直接关联到文件。这种配置适用于频繁读写大量数据的场景,因为它减少了缓存管理的开销,提高了数据传输的实时性。
名词解释
- mmap cached:指通过
PROT_READ | PROT_WRITE
标志进行内存映射,内存页会被缓存到文件系统缓存中。 - mmap nocached:指通过
PROT_READ | PROT_WRITE | MAP_NOCACHE
标志进行内存映射,内存页不会被缓存到文件系统缓存中。
示例代码
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("file.txt", O_RDWR);
char *ptr = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_NOCACHE, fd, 0);
// 使用 ptr 直接从文件中读取和写入数据
// ...
munmap(ptr, 100);
close(fd);
return 0;
}
4. 实验数据对比
4.1 测试环境
- 系统:QNX Neutrino RTOS 7.1
- 硬件:ARM Cortex-A53 系列处理器
4.2 测试设置
本文的测试代码分为以下几部分:
- mmap cached:使用缓存的 mmap 映射。
- mmap nocached:禁用缓存的 mmap 映射。
- memcpy:标准的 C 语言
memcpy
函数。 - invalidate memcpy:在
memcpy
前使用msync
进行缓存同步。 - asm memcpy:基于 ARM 汇编语言实现的快速
memcpy
。
4.3 测试数据
以下表格展示了不同情况下的内存拷贝速度(单位:MB/s):
测试场景 | memcpy (cached) | memcpy (nocached) | invalidate memcpy (cached) | invalidate memcpy (nocached) | asm memcpy (cached) | asm memcpy (nocached) |
---|---|---|---|---|---|---|
速度 (MB/s) | 2133.333333 | 116.363636 | 1333.333333 | 112.280702 | 2133.333333 | 225.352113 |
4.4 代码示例
memcpy
标准的内存拷贝操作,适用于大多数场景。
for (size_t i = 0; i < count; i++) {
memcpy(dst, src[i], bytes);
}
invalidate memcpy
在调用 memcpy
前,通过 msync
函数将共享内存区的内容无效化,以确保数据的一致性。
for (size_t i = 0; i < count; i++) {
msync(src[i], bytes, MS_INVALIDATE);
memcpy(dst, src[i], bytes);
}
asm memcpy
使用汇编指令优化的内存拷贝函数,以最大化性能,适用于对性能要求极高的场景。
inline void aarch64_fast_memcpy(void *dst, const void *src, size_t size) {
#ifdef _QNX_
void *ss = (void *)src, *dd = (void *)dst;
size_t sz = size;
asm volatile("loop_start: "
"ldp q3, q4,[%0,#0x0]\n"
"ldp q5, q6, [%0,#0x20]\n"
"ldp q7, q8, [%0,#0x40]\n"
"ldp q9, q10, [%0,#0x60]\n"
"stp q3, q4, [%1,#0x0]\n"
"stp q5, q6, [%1,#0x20]\n"
"stp q7, q8, [%1,#0x40]\n"
"stp q9, q10, [%1,#0x60]\n"
"add %0, %0, #0x80\n"
"add %1, %1, #0x80\n"
"subs %2, %2, #0x80\n"
"b.ne loop_start\n"
"dsb sy\n"
: /* no output */
: "r"(ss), "r"(dd), "r"(sz));
#endif
}
// ......
for (size_t i = 0; i < count; i++) {
aarch64_fast_memcpy(dst, src[i], bytes);
}
aarch64_fast_memcpy 直接使用了 SIMD(Single Instruction Multiple Data)指令,如 ldp 和 stp,这些指令可以一次加载和存储128位(两个64位的寄存器),在处理大块数据时能极大地提高吞吐量。aarch64_fast_memcpy 的实现通过批量加载和存储数据,可能更好地利用了 CPU 的流水线和内存带宽。
5. 分析与结论
5.1 Cache 模式下的测试结果
- memcpy cached: 2133.33 MB/s
- invalidate memcpy cached: 1333.33 MB/s
- asm memcpy cached: 2133.33 MB/s
在启用缓存的情况下,标准的 memcpy
和汇编实现的 asm memcpy
表现出了相同的高效速度。这表明,在大多数情况下,标准库的 memcpy
已经进行了充分的优化。invalidate memcpy
因为多了一步缓存同步操作,所以速度相对较慢。
5.2 NoCache 模式下的测试结果
- memcpy nocached: 116.36 MB/s
- invalidate memcpy nocached: 112.28 MB/s
- asm memcpy nocached: 225.35 MB/s
在禁用缓存的情况下,汇编实现的 asm memcpy
明显比标准的 memcpy
更快。这是因为在没有缓存的情况下,直接访问内存的速度会受到更大的影响,而汇编实现的优化使得它能够在更低的开销下处理数据。
5.3 结论
通过实验数据,可以得出以下结论:
-
缓存情况下:
memcpy
和asm memcpy
的速度几乎相同,均表现出极高的效率,而invalidate memcpy
的速度明显较慢。这表明在缓存开启的情况下,标准memcpy
已经非常高效,使用汇编优化的内存拷贝不会显著提升性能。 -
非缓存情况下:
asm memcpy
的速度远超其他方法,尤其是标准memcpy
和invalidate memcpy
。这说明在非缓存场景下,使用汇编优化的内存拷贝可以显著提升数据传输效率,是性能敏感应用的优选。
6. 结论
在 QNX 系统中,启用缓存可以显著提高数据传输的效率;而禁用缓存时,使用汇编优化的 asm memcpy
能够提供更好的性能。实际应用中,选择是否启用缓存应根据具体的应用场景和性能需求来决定。
7. 附完整测试C++源码
// mmap_memcpy.c
#include <errno.h>
#include <fcntl.h>
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#ifndef _QNX_
#define PROT_NOCACHE 0
#endif
inline void aarch64_fast_memcpy(void *dst, const void *src, size_t size) {
#ifdef _QNX_
void *ss = (void *)src, *dd = (void *)dst;
size_t sz = size;
asm volatile("loop_start: "
"ldp q3, q4,[%0,#0x0]\n"
"ldp q5, q6, [%0,#0x20]\n"
"ldp q7, q8, [%0,#0x40]\n"
"ldp q9, q10, [%0,#0x60]\n"
"stp q3, q4, [%1,#0x0]\n"
"stp q5, q6, [%1,#0x20]\n"
"stp q7, q8, [%1,#0x40]\n"
"stp q9, q10, [%1,#0x60]\n"
"add %0, %0, #0x80\n"
"add %1, %1, #0x80\n"
"subs %2, %2, #0x80\n"
"b.ne loop_start\n"
"dsb sy\n"
: /* no output */
: "r"(ss), "r"(dd), "r"(sz));
#endif
}
off_t offset(unsigned int bytes) {
static off_t base_offset = 0x1E000000;
off_t return_base_offset = base_offset;
base_offset += bytes;
return return_base_offset;
}
void *mmap_memory(unsigned int bytes, int flag) {
int fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0) {
printf("open /dev/mem failed: %s\n", strerror(errno));
}
void *ptr = mmap(NULL, bytes, flag, MAP_SHARED, fd, offset(bytes));
close(fd);
if (MAP_FAILED == ptr) {
printf("mmap failed: %s\n", strerror(errno));
}
return ptr;
}
void *mmap_memory_cached(unsigned int bytes) {
return mmap_memory(bytes, PROT_READ | PROT_WRITE);
}
void *mmap_memory_nocached(unsigned int bytes) {
return mmap_memory(bytes, PROT_READ | PROT_WRITE | PROT_NOCACHE);
}
// if C++
#ifdef __cplusplus
#include <chrono>
using namespace std::chrono;
#define start() auto start_ = high_resolution_clock::now();
#define end() \
auto end_ = high_resolution_clock::now(); \
double bytes_mb = bytes * count / 1024.0 / 1024.0; \
double cost_ns = duration_cast<nanoseconds>(end_ - start_).count(); \
double mps = bytes_mb / cost_ns * 1e9;
#else
#include <time.h>
#include <unistd.h>
double time_diff_ns(struct timespec start, struct timespec end) {
return (end.tv_sec - start.tv_sec) * 1e9 + (end.tv_nsec - start.tv_nsec);
}
#define start() \
struct timespec start_, end_; \
clock_gettime(CLOCK_REALTIME, &start_);
#define end() \
clock_gettime(CLOCK_REALTIME, &end_); \
double bytes_mb = bytes * count / 1024.0 / 1024.0; \
double cost_ns = time_diff_ns(start_, end_); \
double mps = bytes_mb / cost_ns * 1e9;
#endif
double memcpy_speed(void *dst, void **src, unsigned int bytes,
unsigned int count) {
start();
for (size_t i = 0; i < count; i++) {
memcpy(dst, src[i], bytes);
}
end();
return mps;
}
double invalidate_memcpy_speed(void *dst, void **src, unsigned int bytes,
unsigned int count) {
start();
for (size_t i = 0; i < count; i++) {
msync(src[i], bytes, MS_INVALIDATE);
memcpy(dst, src[i], bytes);
}
end();
return mps;
}
double asm_memcpy_speed(void *dst, void **src, unsigned int bytes,
unsigned int count) {
start();
for (size_t i = 0; i < count; i++) {
aarch64_fast_memcpy(dst, src[i], bytes);
}
end();
return mps;
}
int main(int argc, char *argv[]) {
const unsigned int count = 2;
unsigned int bytes =
128 * 1024; // 128 KB
if (argc > 1) {
bytes = atoi(argv[1]) * 1024;
}
printf("bytes: %d\n", bytes);
printf("count: %d\n", count);
void *mmap_cached_src[count];
void *mmap_nocached_src[count];
for (size_t i = 0; i < count; i++) {
mmap_cached_src[i] = mmap_memory_cached(bytes);
mmap_nocached_src[i] = mmap_memory_nocached(bytes);
}
void *dst = malloc(bytes);
printf("memcpy cached speed: %f MB/s\n",
memcpy_speed(dst, mmap_cached_src, bytes, count));
printf("memcpy nocached speed: %f MB/s\n",
memcpy_speed(dst, mmap_nocached_src, bytes, count));
printf("invalidate memcpy cached speed: %f MB/s\n",
invalidate_memcpy_speed(dst, mmap_cached_src, bytes, count));
printf("invalidate memcpy nocached speed: %f MB/s\n",
invalidate_memcpy_speed(dst, mmap_nocached_src, bytes, count));
printf("asm memcpy cached speed: %f MB/s dst[0]=%d\n",
asm_memcpy_speed(dst, mmap_cached_src, bytes, count), ((char *)dst)[0]);
printf("asm memcpy nocached speed: %f MB/s\n",
asm_memcpy_speed(dst, mmap_nocached_src, bytes, count));
free(dst);
for (size_t i = 0; i < count; i++) {
munmap(mmap_cached_src[i], bytes);
munmap(mmap_nocached_src[i], bytes);
}
return 0;
}