尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Aurix TC234嵌入式JSON解析实战:cJSON与JSMN内存性能深度对比

Aurix TC234嵌入式JSON解析实战:cJSON与JSMN内存性能深度对比 1. 项目缘起为什么要在Aurix上折腾JSON库最近在做一个基于英飞凌Aurix TC234的网关项目需要处理来自多个传感器的配置数据和状态上报。数据格式最初用的是自定义的二进制协议虽然效率高但每次增减字段或者调整结构都得同步更新上位机配置工具和下位机解析代码维护起来非常头疼。团队里有人提议“要不试试JSON现在很多云端接口和配置都用这个通用性好。”这个想法立刻引起了我的兴趣但也带来了一个核心疑问在Aurix这种资源受限的汽车级MCU上跑JSON解析和生成到底现不现实性能开销有多大会不会把宝贵的CPU时间和内存给榨干了带着这些疑问我决定动手做个实验。目标很明确在Aurix TC234开发板上实测两个在嵌入式领域比较有代表性的C语言JSON库——cJSON和JSMN。我不只是想看看它们能不能“跑起来”更想摸清楚它们的“脾气”谁解析更快谁更省内存在只有几百KB RAM的MCU上处理一个嵌套了几层、带数组的典型设备状态JSON报文会不会导致堆栈溢出这些实战数据对于后续是否在车规项目里引入JSON以及如何选型至关重要。2. 实验环境搭建与核心考量工欲善其事必先利其器。实验的第一步是把环境搭好并且明确测试的边界条件。这不是在PC上跑个Benchmark所有选择都必须贴合Aurix TC234的实际应用场景。2.1 硬件与工具链选择我手头用的是英飞凌的AURIX™ TC234 Lite Kit开发板。这颗芯片是典型的Aurix TriCore架构主频200MHz片上集成512KB的Data SRAM和2.5MB的Program Flash。对于汽车电子控制单元来说这个资源不算少但和Linux服务器动辄几个G的内存比起来还是得精打细算。编译器用的是Tasking for TriCore v6.3r2优化等级设置为-O2这是一个在代码大小和运行速度之间比较平衡的选项。调试器是劳特巴赫的Trace32用它来抓取精确的函数执行时间周期数CPU Cycles比用软件定时器更准确。所有测试代码都放在内部RAM中执行以避免Flash访问延迟带来的性能干扰。2.2 测试用例设计模拟真实场景测试用的JSON字符串不能太简单也不能太学术化。我设计了一个模拟智能电池管理单元上报的JSON报文它包含了我在实际项目中常见的几种数据结构{ device_id: BMS_Unit_001, timestamp: 1685432100, status: normal, voltages: [3.65, 3.67, 3.66, 3.64, 3.68, 3.63], temperatures: {sensor_a: 25.5, sensor_b: 26.1, sensor_c: 24.8}, cell_balance: [true, false, true, false, false, true], config: { sample_rate: 100, alarm_threshold: {over_voltage: 4.2, under_voltage: 2.8, over_temp: 60} } }这个报文大约有350个字符。它包含了字符串、整数、浮点数、布尔值、数组数值数组和布尔数组、嵌套对象等JSON基本元素。解析它需要库能够完整地处理这些类型并构建出对应的内存树或标记。2.3 两个候选库的简要介绍与集成cJSON这是一个非常流行的、单文件C语言JSON解析器。它的特点是功能完整API丰富使用起来像操作一个链表树。你调用cJSON_Parse()后会得到一个cJSON*的根节点指针然后可以通过cJSON_GetObjectItem()、cJSON_GetArrayItem()等函数像遍历链表一样访问任何数据。它的缺点是为了这种便利性它会在堆上动态分配大量的小内存块来构建整个树形结构每个键、值、对象、数组都是一个独立的cJSON结构体。JSMN它的全称是“JSON Scanner Minimalistic”顾名思义它追求极致的简洁和最小开销。JSMN本身只是一个“词法分析器”或“标记器”。它不会帮你分配任何内存也不会构建树形结构。它的工作是把JSON字符串从头到尾扫描一遍告诉你哪里是对象开始、哪里是键、哪里是值、值的类型是什么。这些信息以一个“标记”数组的形式返回每个标记只包含类型、起始位置和长度。后续的数据提取工作需要你根据这些标记信息自己到原始的JSON字符串里去“抠”出来。集成过程本身不复杂。对于cJSON就是把cJSON.c和cJSON.h加入工程注意其内部使用了标准库的malloc和free。在Aurix上我重写了cJSON_malloc和cJSON_free这两个宏将其指向我工程里经过验证的、线程安全的内存池接口这是嵌入式开发的一个好习惯。对于JSMN就更简单了只有一个头文件jsmn.h将其包含即可。3. 内存使用深度剖析静态与动态的博弈在资源受限的MCU上内存往往是比CPU速度更紧张的资源。因此我把内存占用分析放在了性能测试之前。3.1 cJSON便利性背后的动态内存代价cJSON的工作方式决定了它的内存使用模式是“解析时集中分配使用后逐个释放”。当我调用cJSON_Parse()解析上面那个350字节的JSON字符串时背后发生了一系列的malloc调用。我通过重写的内存分配函数打了日志统计结果如下为了完整构建这个JSON的内存树cJSON总共发起了28次内存分配请求。这包括1个根对象7个顶层键值对如device_id,timestamp等2个数组对象voltages和cell_balance6个浮点数节点voltages数组内的元素6个布尔值节点cell_balance数组内的元素1个嵌套的temperatures对象及其3个子节点1个嵌套的config对象及其子节点sample_rate和嵌套的alarm_threshold对象每次分配的大小并不大主要是cJSON结构体本身在Tasking编译器下约为48字节以及为字符串值分配的额外空间。但积少成多所有分配的总内存开销达到了约1.8KB。这还只是解析一个报文如果系统需要同时缓存多个报文或者报文更大更复杂内存压力会急剧上升。注意这里的1.8KB是峰值堆内存占用。这意味着在调用cJSON_Delete()释放之前这部分内存一直被占用。在内存只有几百KB的系统中频繁地解析和释放不同报文会导致堆内存碎片化长期运行可能有内存分配失败的风险。3.2 JSMN极简主义与零拷贝哲学JSMN采取了完全不同的策略。它的核心函数jsmn_parse()只需要两个外部资源一个jsmntok_t类型的标记数组和原始的JSON字符串。它不分配任何堆内存。标记数组这是一个需要用户预先定义好的静态数组比如jsmntok_t tokens[64];。每个标记只包含三个整数类型如对象、字符串、数字、起始位置在字符串中的索引、长度。在我们的测试用例中JSMN总共产生了42个标记。每个jsmntok_t结构体占12字节因此整个标记数组的内存开销是固定的42 * 12 504字节并且是栈上或静态存储区的开销没有堆碎片问题。零拷贝JSMN不复制任何字符串。当它告诉你某个值的类型是字符串起始位置是120长度是5时你需要自己从json_string[120]开始手动拷贝或处理这5个字符。这意味着原始的JSON字符串必须在整个使用周期内保持有效且不被修改。这通常要求你将接收到的JSON报文存放在一个固定的缓冲区中。内存占用对比总结特性cJSONJSMN内存分配方式动态 (malloc)大量小对象静态用户预分配标记数组峰值堆内存~1.8 KB (解析测试用例)0 KB额外存储开销每个节点约48字节结构体每个标记12字节数据访问方式通过结构体指针直接访问已解析的值需根据标记索引回原字符串提取原始字符串解析后可以释放必须保持有效适用场景需要频繁、随机访问JSON树结构复杂且多变内存极度受限仅需提取少数字段流式解析这个对比非常鲜明。cJSON用空间换取了时间和编程的便利性而JSMN用更复杂的编程接口换取了极致的空间效率。如果你的应用内存充裕且JSON结构复杂需要频繁遍历cJSON是更舒适的选择。反之如果你的内存以KB计或者只需要从一个大JSON里提取一两个字段JSMN几乎是唯一的选择。4. 解析性能实测CPU周期下的真相内存占用是静态成本而解析性能则关系到系统的实时响应能力。我使用Trace32在指令级精确测量了从调用解析函数开始到完成解析对于cJSON是得到树根对于JSMN是得到所有标记所消耗的CPU周期数。为了结果稳定每个测试都循环执行了1000次取平均值。4.1 cJSON性能表现解析我们设计的测试JSONcJSON平均耗时约 21,800 个CPU周期。在主频200MHz的TC234上这大约是0.109毫秒。这个速度对于很多非实时性应用来说是完全可以接受的。cJSON的解析过程可以粗略分为两步1词法分析分词2语法分析与树构建。它的性能开销主要来自第二部分——为每一个识别出的元素创建cJSON节点、分配内存、拷贝字符串对于字符串类型的值。我们的JSON中有多个字符串键和值还有浮点数需要调用strtod进行字符串到浮点的转换这些操作都比较耗时。4.2 JSMN性能表现JSMN的解析耗时仅为约 9,500 个CPU周期换算成时间是0.0475毫秒。比cJSON快了一倍多。这个结果在意料之中。JSMN只做纯粹的词法扫描它像一台高速扫描仪只识别JSON的语法结构哪里是{哪里是key哪里是:value是什么类型并记录下位置。它不做内存分配不做字符串拷贝也不进行数值转换。所有“重量级”的操作都被推迟到了用户按需提取数据的时候。4.3 综合性能考量解析 vs 访问然而性能比较不能只看解析时间。我们必须考虑“访问数据”的成本。使用cJSON解析完成后获取device_id的值只需要一行代码char *id cJSON_GetObjectItem(root, device_id)-valuestring;。这个操作是O(1)的因为它只是在已经构建好的链表中查找。使用JSMN解析完成后你得到的只是一个标记数组。要获取device_id你需要1遍历标记数组找到键为device_id的字符串标记2根据下一个标记即值标记的位置和长度从原始字符串中手动截取出子字符串3如果需要字符串拷贝还得自己malloc或strcpy。这个过程是O(n)的并且包含了额外的处理逻辑。因此一个更公平的性能公式是总耗时 解析耗时 (访问次数 * 单次访问耗时)。如果你的应用需要在解析后反复、随机地访问JSON中的大量字段那么cJSON的总体验效可能更高。如果你的应用模式是“解析一次提取几个固定字段然后就不再使用”那么JSMN的“快速解析 按需慢速提取”模式总耗时可能更低。5. 实际编码体验与避坑指南纸上得来终觉浅绝知此事要躬行。把这两个库集成到Aurix的工程里并写出健壮的业务代码过程中遇到了几个值得分享的坑。5.1 使用cJSON的注意事项内存分配器必须重定向这是最重要的一步。默认的malloc/free在无操作系统的嵌入式环境中可能不可用或者不是线程安全的。务必在包含cJSON.h之前定义CJSON_CDECL和CJSON_STDCALL如果不需要可忽略并重写cJSON_malloc和cJSON_free。我将其指向了一个带锁的内存池确保了在多核AurixTC234有2个核任务间调用的安全性。// 示例重定向到内存池 #define cJSON_malloc(size) my_mempool_alloc(size) #define cJSON_free(ptr) my_mempool_free(ptr)检查每一个返回值cJSON_Parse()在失败时返回NULL。cJSON_GetObjectItem()在找不到项时也返回NULL。任何直接对返回指针进行解引用如-valuestring的操作都必须在前一步进行非空判断否则硬件错误HardFault随时可能发生。字符串值的生命周期通过cJSON_GetObjectItem(item, key)-valuestring获取的字符串指针指向的是cJSON内部分配的内存。当你调用cJSON_Delete()释放整棵树时这些字符串内存也会被一并释放。如果你需要长期持有这个字符串必须立即用strdup或类似函数将其拷贝到自己的缓冲区中。浮点数精度cJSON内部使用C标准库的strtod进行转换。在嵌入式环境中如果启用了FPU这没问题。但要注意字符串形式的浮点数可能会带来精度损失和性能开销。对于固定精度的数值如电压值3.65有时用整数3650表示3.650V在JSON中传递在代码中再除以1000是更高效、更精确的做法。5.2 使用JSMN的实战技巧预先分配足够的标记jsmn_parse()的第三个参数是标记数组的大小。如果JSON太复杂标记数量超过这个大小函数会返回JSMN_ERROR_NOMEM。一个保守的估计方法是标记数 ≈ JSON中所有大括号、中括号、冒号、逗号、键、值的总数。对于不确定大小的JSON可以采取两遍解析第一遍用jsmn_parse()传入NULL标记数组它会返回需要的标记数量然后分配足够大的数组再进行第二遍正式解析。理解标记数组的结构JSMN的标记是平铺在数组里的但它通过parent和size等字段隐含了层级关系。编写一个递归函数来打印或遍历标记结构是上手JSMN的最佳方式。你需要自己处理对象、数组的嵌套关系。手动提取数据这是最繁琐但也最核心的部分。例如要提取整数sample_rate// 假设 tokens 是标记数组json 是原始字符串 for (int i 0; i num_tokens; i) { if (tokens[i].type JSMN_STRING) { // 比较键名是否为 sample_rate if (jsoneq(json, tokens[i], sample_rate) 0) { // 下一个标记就是值 jsmntok_t *v tokens[i1]; char value_str[32]; memcpy(value_str, json[v-start], v-end - v-start); value_str[v-end - v-start] \0; int sample_rate atoi(value_str); break; } } }你需要自己写jsoneq这样的辅助函数来比较字符串自己处理数字转换。代码量会显著增加。原始缓冲区管理由于JSMN是零拷贝的你必须确保存放原始JSON字符串的缓冲区生命周期足够长并且在解析和访问阶段不会被其他任务修改。通常需要设计一个环形缓冲区或静态缓冲区池来管理这些报文。6. 选型决策框架没有银弹只有权衡经过上面的实验和分析可以清楚地看到cJSON和JSMN代表了嵌入式JSON处理的两种不同哲学。选择哪一个不是一个技术优劣问题而是一个工程权衡问题。我总结了一个简单的决策框架可以根据你的项目需求来对号入座。优先选择 cJSON 的情况项目内存相对充裕有几十KB甚至上百KB的RAM可以用于动态内存分配且对内存碎片化有监控和管理手段如使用内存池。JSON结构复杂且访问模式随机需要频繁地、以任意顺序访问JSON中的多个字段甚至需要修改JSON树并重新序列化成字符串。开发效率优先项目时间紧希望使用成熟、API友好的库来快速实现功能减少底层字符串处理带来的编码错误和调试时间。需要修改或生成JSONcJSON提供了完整的创建和修改JSON树的API这在需要动态组装上报报文时非常方便。优先选择 JSMN 的情况内存极度受限RAM资源以KB计必须杜绝任何不可预测的堆内存分配。静态分配是唯一选择。仅需提取少数字段协议固定每次只需要从一个大JSON报文中提取出固定的几个字段如状态码、时间戳其他数据可以忽略。JSMN“快速扫描按需提取”的模式效率最高。处理流式JSON或超大JSONJSON数据是分块到达的或者整个JSON太大无法一次性装入内存。JSMN可以配置为增量解析模式这是cJSON不具备的特性。追求极致的解析速度和确定性对实时性要求极高需要确保解析阶段的时间开销是稳定且可预测的。JSMN的纯扫描操作非常稳定。在Aurix TriCore上的特别建议对于典型的汽车电子应用如车身控制器、电池管理系统等我个人的倾向是在通信协议层面优先考虑更紧凑的二进制格式。但如果因为上下游生态如与云端JSON接口对接必须使用JSON那么对于简单的配置下发例如通过UDS或DoIP下发的参数配置JSON报文小解析后立即使用并丢弃JSMN是更安全、更节省资源的选择。它的静态特性避免了动态内存管理在长期运行中的潜在风险。对于复杂的状态上报或数据聚合例如需要收集多个传感器数据组装成一个复杂的JSON上报给网关可以考虑使用cJSON。因为“生成”JSON是cJSON的强项而且在上报前数据已经在内存中以结构体形式存在组装成JSON树的开销相对可控。关键是要使用定制的内存分配器并将其生命周期限制在单个上报周期内用完即删。最后无论选择哪个库都强烈建议编写完善的单元测试覆盖各种畸形的JSON输入键值缺失、括号不匹配、非法字符等确保解析器的鲁棒性不会成为系统的单点故障。在Aurix这样的安全关键系统中数据的可靠解析是功能安全的基石之一。
返回列表