最近在给我的 MUD 项目做数据互通功能,架构设计是 LPC 端向 Go 中间件层批量发送玩家数据存档、排行榜数据做持久化。在这个过程中,我遇到了一个极其痛苦的瓶颈:现有的 LPC 软实现(simul_efun/json.c)在大数据量下实在是太慢了。
每当保存玩家完整档案或者导出大量日志时,服务器就会出现肉眼可见的卡顿,Eval Cost 更是瞬间爆炸。尤其是 json_decode,更是性能消耗大户,稍不注意就触发 Too long evaluation 错误。
于是我决定自己造个轮子——利用 FluffOS 的 Package 机制,集成高性能 C 语言库 yyjson,实现原生的驱动级 JSON 支持。
结果令人震惊:json_encode 性能提升 100 倍,json_decode 性能提升了整整 600 倍! 以前要跑 2-3 秒的任务,现在“秒开”,完全没有停顿感。
不想看测试的直接往下拉到源码分享~~
⚔️ 硬核实测:数据对比
口说无凭,我写了一个专门的压力测试脚本。
测试场景
遍历玩家背包中所有物品(包含完整 dbase),分别用 原生 efun 和 老旧 sfun 进行循环序列化与反序列化压测。由于 sfun 效率极低,在大规模数据下会直接顶到 Eval 限制导致驱动罢工,因此我们将循环次数设定为 1000次。
测试脚本 (benchmark_json.c)
// 💾 测试脚本:benchmark_json.c
// ==========================================
mapping data_map = ([]);
string json_string;
object *inv;
int i, count;
int cost_e_enc, cost_s_enc;
int cost_e_dec, cost_s_dec;
int test_times = 1000;
// 1. 数据准备
inv = all_inventory(me);
if (!sizeof(inv)) {
return notify_fail("你的背包里没有任何物品,无法生成测试数据。\n");
}
count = 0;
foreach (object ob in inv) {
mapping dbase = ob->query_entire_dbase();
if (mapp(dbase)) {
count++;
data_map[count + ""] = dbase;
}
}
// 预生成一个标准 JSON 字符串供 Decode 测试使用
json_string = json_encode(data_map);
debug(sprintf("\n[数据初始化] 包含 %d 个物品属性,JSON 长度: %d 字节", count, strlen(json_string)));
debug(sprintf("[测试规模] 循环次数: %d 次\n", test_times));
// 2. Encode (序列化) 测试
cost_e_enc = time_expression {
for (i = 0; i < test_times; i++) json_encode(data_map);
};
cost_s_enc = time_expression {
for (i = 0; i < test_times; i++) "/adm/simul_efun/json"->json_encode(data_map);
};
// 3. Decode (反序列化) 测试
cost_e_dec = time_expression {
for (i = 0; i < test_times; i++) json_decode(json_string);
};
cost_s_dec = time_expression {
for (i = 0; i < test_times; i++) "/adm/simul_efun/json"->json_decode(json_string);
};
// 4. 结果汇报
debug("----------------------------------------------------------\n");
debug(sprintf("% -15s | % -12s | % -12s | % -10s\n", "操作类型", "efun 耗时", "sfun 耗时", "性能差距"));
debug("----------------------------------------------------------\n");
debug(sprintf("% -15s | % -10d ms | % -10d ms | % -.2f 倍\n",
"JSON Encode", cost_e_enc/1000, cost_s_enc/1000, to_float(cost_s_enc)/to_float(cost_e_enc)));
debug(sprintf("% -15s | % -10d ms | % -10d ms | % -.2f 倍\n",
"JSON Decode", cost_e_dec/1000, cost_s_dec/1000, to_float(cost_s_dec)/to_float(cost_e_dec)));
debug("----------------------------------------------------------\n");
📊 测试结果截图
在我的开发机上(1000次循环,背包10件物品),测试结果如下:
- sfun (LPC 实现):序列化耗时 194ms,反序列化耗时 5767ms。
- efun (原生驱动):序列化耗时 1ms,反序列化耗时 194ms。
- 结论:序列化快了 100倍,反序列化快了 600倍 以上!
💎 杀手级功能:write_json 与 read_json
除了速度快,我专门设计了两个直接读写文件的函数,用来彻底解决 MAX_STRING_LENGTH 的限制问题。
write_json(file, data):直接将序列化数据从 C++ 缓冲区流式写入磁盘。由于不经过 LPC 字符串栈,你可以轻松导出一个 100MB 的 JSON 文件,而不会触发任何字符串长度限制错误。read_json(file):直接从磁盘读取并解析。它绕过了read_file()的长度约束,是加载大型数据库存档的利器。
当然了,还是建议不要读取写入太大的文件! ··
测试截图
··
API 文档 (JSDoc)
/**
* @function json_encode
* @description 将 LPC 数据编码为 JSON 字符串。
* @param {mixed} data - 待编码数据。
* @returns {string} JSON 字符串。
*/
string json_encode(mixed data);
/**
* @function json_decode
* @description 将 JSON 字符串解析为 LPC 数据。
* @param {string} json - JSON 字符串。
* @returns {mixed} LPC Mapping 或 Array。
*/
mixed json_decode(string json);
/**
* @function write_json
* @description 将数据序列化并直接流式写入文件(绕过 MAX_STRING_LENGTH 限制)。
* @param {string} file_path - 目标文件路径(需符合 valid_write 权限)。
* @param {mixed} data - 待保存数据。
* @returns {int} 成功返回 1。
*/
int write_json(string file_path, mixed data);
/**
* @function read_json
* @description 直接从文件读取 JSON 并解析(绕过 MAX_STRING_LENGTH 限制)。
* @param {string} file_path - 文件路径(需符合 valid_read 权限)。
* @returns {mixed} 解析后的 LPC 数据。
*/
mixed read_json(string file_path);
🛠️ 实现指南:如何集成?
1. 目录结构
在 fluffos/src/packages/json_extension/ 下放入以下文件:
json_extension.spec(函数声明)string json_encode(mixed); mixed json_decode(string); int write_json(string, mixed); mixed read_json(string);-
pkg_json.cc(C++ 实现)#include "base/package_api.h" #include "vm/internal/base/mapping.h" #include "vm/internal/base/array.h" #include "vm/internal/base/object.h" #include "packages/core/file.h" #include "yyjson.h" #include <vector> #include <unordered_set> #include <algorithm> #include <string> #include <cstdio> #include <cstring> #include <climits> // ========================================================================= // 编译期优化宏 // ========================================================================= #if defined(__GNUC__) || defined(__clang__) #define LIKELY(x) __builtin_expect(!!(x), 1) #define UNLIKELY(x) __builtin_expect(!!(x), 0) #else #define LIKELY(x) (x) #define UNLIKELY(x) (x) #endif // ========================================================================= // 配置常量 // ========================================================================= #define MAX_JSON_DEPTH 128 #define MAX_JSON_FILE_SIZE (256 * 1024 * 1024) #define MAX_JSON_STRING_LENGTH (64 * 1024 * 1024) #define MAX_JSON_ARRAY_SIZE 10000000 #define MAX_JSON_OBJECT_SIZE 5000000 #define CIRCULAR_CHECK_THRESHOLD 24 // 深度超过 24 启用 Hash 加速 // ========================================================================= // RAII 辅助类 // ========================================================================= struct ScopedYYDoc { yyjson_doc* doc; explicit ScopedYYDoc(yyjson_doc* d) : doc(d) {} ~ScopedYYDoc() { if (doc) yyjson_doc_free(doc); } }; struct ScopedYYMutDoc { yyjson_mut_doc* doc; explicit ScopedYYMutDoc(yyjson_mut_doc* d) : doc(d) {} ~ScopedYYMutDoc() { if (doc) yyjson_mut_doc_free(doc); } }; // ========================================================================= // 快速整数转字符串 // ========================================================================= static inline int fast_i64toa(int64_t value, char* buffer) { if (UNLIKELY(value == 0)) { buffer[0] = '0'; buffer[1] = '\0'; return 1; } char temp[24]; char* p = temp; uint64_t uval = static_cast<uint64_t>(value); if (value < 0) { uval = ~uval + 1; } while (uval > 0) { *p++ = static_cast<char>('0' + (uval % 10)); uval /= 10; } int len = 0; if (value < 0) buffer[len++] = '-'; while (p > temp) { buffer[len++] = *--p; } buffer[len] = '\0'; return len; } // ========================================================================= // 混合循环检测器 v4.1 // ========================================================================= class CircularChecker { private: std::vector<void*> stack_; // 存储全量路径,保证正确性 std::unordered_set<void*> deep_set_; // 深层加速索引 public: CircularChecker() { stack_.reserve(64); // 预分配,减少动态扩容 } // v4.1 优化:传入 depth 避免浅层时检查空 Hash inline bool contains(void* ptr, int depth) const { // 策略:优先检查 Hash (O(1)),深层循环最危险需最快拦截 // 优化:浅层时 deep_set_ 必然为空,直接跳过 if (depth >= CIRCULAR_CHECK_THRESHOLD && !deep_set_.empty()) { if (deep_set_.find(ptr) != deep_set_.end()) { return true; } } // 兜底:线性扫描 Vector (O(N)) // Vector 内存连续,CPU 预取器友好,无需拆分扫描 for (auto it = stack_.rbegin(); it != stack_.rend(); ++it) { if (*it == ptr) return true; } return false; } inline void insert(void* ptr, int depth) { stack_.push_back(ptr); if (depth >= CIRCULAR_CHECK_THRESHOLD) { deep_set_.insert(ptr); } } inline void remove(void* ptr, int depth) { stack_.pop_back(); if (depth >= CIRCULAR_CHECK_THRESHOLD) { deep_set_.erase(ptr); } } }; // ========================================================================= // Encoder: LPC -> JSON // ========================================================================= static yyjson_mut_val* svalue_to_json_impl(yyjson_mut_doc* doc, svalue_t* sv, CircularChecker* checker, int depth) { if (UNLIKELY(depth > MAX_JSON_DEPTH)) { return yyjson_mut_str(doc, "<error: max depth reached>"); } switch (sv->type) { case T_NUMBER: return yyjson_mut_int(doc, sv->u.number); case T_REAL: return yyjson_mut_real(doc, sv->u.real); case T_STRING: return yyjson_mut_str(doc, sv->u.string); case T_ARRAY: { array_t* arr = sv->u.arr; int size = arr->size; if (UNLIKELY(size > MAX_JSON_ARRAY_SIZE)) { debug_message("json_encode: array size %d exceeds limit %d, encoding anyway\n", size, MAX_JSON_ARRAY_SIZE); } // v4.1: 传入 depth 参数 if (UNLIKELY(checker->contains(arr, depth))) { return yyjson_mut_str(doc, "<circular_ref_array>"); } checker->insert(arr, depth); yyjson_mut_val* json_arr = yyjson_mut_arr(doc); for (int i = 0; i < size; i++) { yyjson_mut_arr_append(json_arr, svalue_to_json_impl(doc, &arr->item[i], checker, depth + 1)); } checker->remove(arr, depth); return json_arr; } case T_MAPPING: { mapping_t* map = sv->u.map; // v4.1: 传入 depth 参数 if (UNLIKELY(checker->contains(map, depth))) { return yyjson_mut_str(doc, "<circular_ref_mapping>"); } checker->insert(map, depth); yyjson_mut_val* json_obj = yyjson_mut_obj(doc); char num_buf[32]; int obj_count = 0; for (int i = 0; i < map->table_size; i++) { for (mapping_node_t *elt = map->table[i]; elt; elt = elt->next) { if (UNLIKELY(++obj_count > MAX_JSON_OBJECT_SIZE)) { debug_message("json_encode: object size exceeds limit %d, truncating\n", MAX_JSON_OBJECT_SIZE); checker->remove(map, depth); return json_obj; } svalue_t *key = &elt->values[0]; svalue_t *val = &elt->values[1]; if (key->type == T_STRING) { yyjson_mut_obj_add(json_obj, yyjson_mut_str(doc, key->u.string), svalue_to_json_impl(doc, val, checker, depth + 1) ); } else if (key->type == T_NUMBER) { int len = fast_i64toa(static_cast<int64_t>(key->u.number), num_buf); yyjson_mut_obj_add(json_obj, yyjson_mut_strn(doc, num_buf, static_cast<size_t>(len)), svalue_to_json_impl(doc, val, checker, depth + 1) ); } } } checker->remove(map, depth); return json_obj; } case T_OBJECT: if (sv->u.ob && !(sv->u.ob->flags & O_DESTRUCTED)) { return yyjson_mut_str(doc, sv->u.ob->obname); } return yyjson_mut_null(doc); default: return yyjson_mut_null(doc); } } // ========================================================================= // Decoder: JSON -> LPC // ========================================================================= static void json_to_svalue(yyjson_val* val, svalue_t* out, int depth) { out->type = T_NUMBER; out->subtype = 0; out->u.number = 0; if (UNLIKELY(!val || depth > MAX_JSON_DEPTH)) return; switch (yyjson_get_type(val)) { case YYJSON_TYPE_NULL: break; case YYJSON_TYPE_BOOL: out->u.number = yyjson_get_bool(val) ? 1 : 0; break; case YYJSON_TYPE_NUM: if (yyjson_is_real(val)) { out->type = T_REAL; out->u.real = yyjson_get_real(val); } else { out->u.number = static_cast<long>(yyjson_get_int(val)); } break; case YYJSON_TYPE_STR: { size_t len = yyjson_get_len(val); if (UNLIKELY(len > MAX_JSON_STRING_LENGTH)) { debug_message("json_decode: string length %zu exceeds limit %d, truncating\n", len, MAX_JSON_STRING_LENGTH); out->type = T_STRING; out->subtype = STRING_MALLOC; out->u.string = string_copy("", "json_decode_overflow"); break; } out->type = T_STRING; out->subtype = STRING_MALLOC; out->u.string = string_copy(yyjson_get_str(val), "json_decode"); break; } case YYJSON_TYPE_ARR: { size_t count = yyjson_arr_size(val); if (UNLIKELY(count > MAX_JSON_ARRAY_SIZE)) { debug_message("json_decode: array size %zu exceeds limit %d, truncating\n", count, MAX_JSON_ARRAY_SIZE); count = MAX_JSON_ARRAY_SIZE; } array_t* lpc_arr = allocate_array(static_cast<int>(count)); out->type = T_ARRAY; out->u.arr = lpc_arr; yyjson_val* item; size_t idx, max; yyjson_arr_foreach(val, idx, max, item) { if (idx >= count) break; json_to_svalue(item, &lpc_arr->item[idx], depth + 1); } break; } case YYJSON_TYPE_OBJ: { size_t count = yyjson_obj_size(val); if (UNLIKELY(count > MAX_JSON_OBJECT_SIZE)) { debug_message("json_decode: object size %zu exceeds limit %d, truncating\n", count, MAX_JSON_OBJECT_SIZE); count = MAX_JSON_OBJECT_SIZE; } mapping_t* lpc_map = allocate_mapping(static_cast<int>(count)); out->type = T_MAPPING; out->u.map = lpc_map; yyjson_val *key, *ele; size_t idx, max; int inserted = 0; yyjson_obj_foreach(val, idx, max, key, ele) { if (static_cast<size_t>(inserted) >= count) break; svalue_t key_sv; key_sv.type = T_STRING; key_sv.subtype = STRING_MALLOC; key_sv.u.string = string_copy(yyjson_get_str(key), "json_key"); svalue_t* dest = find_for_insert(lpc_map, &key_sv, 1); if (dest) { json_to_svalue(ele, dest, depth + 1); inserted++; } free_string_svalue(&key_sv); } break; } } } // ========================================================================= // EFUNS 实现 // ========================================================================= void f_json_encode(void) { svalue_t* arg = sp; yyjson_mut_doc* doc = yyjson_mut_doc_new(NULL); if (UNLIKELY(!doc)) { pop_n_elems(1); push_number(0); return; } ScopedYYMutDoc doc_guard(doc); CircularChecker checker; yyjson_mut_val* root = svalue_to_json_impl(doc, arg, &checker, 0); yyjson_mut_doc_set_root(doc, root); size_t len; char* json_str = yyjson_mut_write(doc, 0, &len); if (UNLIKELY(!json_str)) { pop_n_elems(1); push_number(0); return; } pop_n_elems(1); copy_and_push_string(json_str); free(json_str); } void f_json_decode(void) { const char* str = sp->u.string; if (UNLIKELY(!str || !*str)) { pop_n_elems(1); push_number(0); return; } yyjson_read_err err; yyjson_doc* doc = yyjson_read_opts(const_cast<char*>(str), strlen(str), 0, NULL, &err); if (UNLIKELY(!doc)) { debug_message("json_decode failed: %s at pos %zu\n", err.msg, err.pos); pop_n_elems(1); push_number(0); return; } ScopedYYDoc doc_guard(doc); svalue_t result; json_to_svalue(yyjson_doc_get_root(doc), &result, 0); pop_n_elems(1); sp++; *sp = result; } void f_read_json(void) { const char* filename = sp->u.string; const char* real_path = check_valid_path(filename, current_object, "read_json", 0); if (UNLIKELY(!real_path)) { pop_n_elems(1); push_number(0); return; } FILE* fp = fopen(real_path, "rb"); if (UNLIKELY(!fp)) { pop_n_elems(1); push_number(0); return; } fseek(fp, 0, SEEK_END); long fsize = ftell(fp); fseek(fp, 0, SEEK_SET); if (UNLIKELY(fsize <= 0 || fsize > MAX_JSON_FILE_SIZE)) { fclose(fp); pop_n_elems(1); push_number(0); return; } // In-Situ 解析优化:节省 50% 内存 // 注意:必须添加 YYJSON_PADDING_SIZE 防止越界读取 size_t buf_size = static_cast<size_t>(fsize) + YYJSON_PADDING_SIZE; char* buffer = static_cast<char*>(malloc(buf_size)); if (UNLIKELY(!buffer)) { fclose(fp); pop_n_elems(1); push_number(0); return; } size_t read_size = fread(buffer, 1, static_cast<size_t>(fsize), fp); fclose(fp); if (UNLIKELY(read_size != static_cast<size_t>(fsize))) { free(buffer); pop_n_elems(1); push_number(0); return; } yyjson_read_err err; // INSITU 模式:直接在 buffer 上解析,避免内部拷贝 yyjson_doc* doc = yyjson_read_opts(buffer, static_cast<size_t>(fsize), YYJSON_READ_INSITU | YYJSON_READ_NOFLAG, NULL, &err); if (UNLIKELY(!doc)) { free(buffer); debug_message("read_json parse error: %s at pos %zu\n", err.msg, err.pos); pop_n_elems(1); push_number(0); return; } ScopedYYDoc doc_guard(doc); svalue_t result; // 关键:string_copy 会将数据拷贝到驱动托管内存 // 绝不可直接引用 buffer 中的字符串,因为 buffer 即将释放 json_to_svalue(yyjson_doc_get_root(doc), &result, 0); // In-Situ 模式下,buffer 必须在 doc 销毁前保持有效 // 但由于 json_to_svalue 中的 string_copy 已完成拷贝,此处释放安全 free(buffer); pop_n_elems(1); sp++; *sp = result; } void f_write_json(void) { svalue_t* data = sp; const char* filename = (sp - 1)->u.string; const char* real_path = check_valid_path(filename, current_object, "write_json", 1); if (UNLIKELY(!real_path)) { pop_n_elems(2); push_number(0); return; } yyjson_mut_doc* doc = yyjson_mut_doc_new(NULL); if (UNLIKELY(!doc)) { pop_n_elems(2); push_number(0); return; } ScopedYYMutDoc doc_guard(doc); CircularChecker checker; yyjson_mut_val* root = svalue_to_json_impl(doc, data, &checker, 0); yyjson_mut_doc_set_root(doc, root); FILE* fp = fopen(real_path, "wb"); if (UNLIKELY(!fp)) { pop_n_elems(2); push_number(0); return; } // 流式写入:直接写文件,避免内存尖峰 yyjson_write_err err; bool success = yyjson_mut_write_fp(fp, doc, 0, NULL, &err); fclose(fp); if (UNLIKELY(!success)) { debug_message("write_json failed: %s (code: %d)\n", err.msg, err.code); } pop_n_elems(2); push_number(success ? 1 : 0); } CMakeLists.txt(编译配置)if(PACKAGE_JSON_EXTENSION) add_library(package_json_extension STATIC pkg_json.cc yyjson.c) target_include_directories(package_json_extension PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../) endif()yyjson.c/h(从 yyjson 官网 获取)
如有问题可联系作者 279631638
💡 给独立开发者的建议
如果你的 MUD 项目也开始越来越多地涉及到 JSON 交互、大数据存储,强烈建议抛弃旧的 json.c。
集成原生 JSON 扩展虽然需要重新编译驱动,但它带来的稳定性(不再爆 Eval Cost)和扩展性(支持超大文件)是 simul_efun 永远无法企及的。
By Muy - 坚持技术驱动,让 MUD 再战十年。
