3d点云项目性能测试
最近整了个点云的项目,遇到些性能问题。本文将会针对这些问题,给出一些小技巧。
在平台上,选择本地的ply点云数据文件展示。效果如下:
发现18MB大小的点云文件,耗时2s+,过长,不能忍,优化之。
算法改进
整体的逻辑:读取文件内容、逐行读取点数据、整理x、y、z、r、g、b,然后threejs渲染显示。现阶段代码:
// 原始实现
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
const parts = line.split(/\s+/);
if (parts.length >= 6) {
const x = parseFloat(parts[0]);
const y = parseFloat(parts[1]);
const z = parseFloat(parts[2]);
const r = parseInt(parts[3], 10);
const g = parseInt(parts[4], 10);
const b = parseInt(parts[5], 10);
// ...
}
}
从代码不难看出,每行创建新数组,内存分配开销大。25万行 × 6个元素 = 150万个字符串对象。量确实大。
优化后:
// 优化实现
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line || line.length < 10) continue; // 快速跳过
let idx1 = line.indexOf(' ', 0);
if (idx1 === -1) continue;
let idx2 = line.indexOf(' ', idx1 + 1);
if (idx2 === -1) continue;
let idx3 = line.indexOf(' ', idx2 + 1);
if (idx3 === -1) continue;
let idx4 = line.indexOf(' ', idx3 + 1);
if (idx4 === -1) continue;
let idx5 = line.indexOf(' ', idx4 + 1);
if (idx5 === -1) idx5 = line.length;
// 直接截取,避免数组创建
const x = parseFloat(line.substring(0, idx1));
const y = parseFloat(line.substring(idx1 + 1, idx2));
const z = parseFloat(line.substring(idx2 + 1, idx3));
const r = parseInt(line.substring(idx3 + 1, idx4), 10);
const g = parseInt(line.substring(idx4 + 1, idx5), 10);
const b = parseInt(line.substring(idx5 + 1), 10);
// ...
}
- indexOf只返回数字,不创建对象
- 时间复杂度 O(1) 到 O(n),但实际很快
- substring直接截取,不创建中间数组
内存优化
现阶段代码:
const points = [];
for (let i = 0; i < vertexCount; i++) {
points.push({ x, y, z, r, g, b });
}
这是一个很小的细节,主要是JavaScript数组的动态扩容机制。他的过程是:创建新数组(2倍大小)、复制所有元素、垃圾回收旧数组。
优化后
const points = new Array(vertexCount);
for (let i = 0; i < vertexCount; i++) {
points[i] = { x, y, z, r, g, b };
}
一次性分配,避免多次扩容,无复制操作,无垃圾回收
非阻塞处理
JavaScript是单线程,长时间解析会阻塞UI,导致页面卡顿。解决方案:让出控制权
具体代码
// 假设有12万个点需要解析
const vertexCount = 120000;
const BATCH_SIZE = 50000;
let pointIndex = 0;
for (let i = 0; i < lines.length; i++) {
// 解析一行数据
const line = lines[i];
// ... 解析逻辑 ...
points[pointIndex] = { x, y, z, r, g, b };
pointIndex++;
// 每处理50000个点,让出控制权
if (pointIndex % BATCH_SIZE === 0) {
// 这行代码做了什么?
await new Promise(resolve => setTimeout(resolve, 0));
// 执行流程:
// 1. setTimeout将resolve函数放入事件队列
// 2. await暂停当前函数执行
// 3. 主线程释放,浏览器可以:
// - 更新DOM(显示进度条)
// - 处理点击事件
// - 渲染动画
// 4. 事件循环执行完其他任务后
// 5. 执行setTimeout的回调(resolve)
// 6. Promise resolve,继续执行for循环
}
}
重点是setTimeout(resolve, 0)。将回调函数放入事件队列,延迟0毫秒执行.就是暂停当前函数执行,让出主线程浏览器可以执行其他任务(更新UI、处理事件等)。事件循环执行完其他任务后,再回来继续执行
流式读取
这个之前讲过很多次,不赘述。
最后看效果:
几轮操作下来,耗时缩减为原来的四分之一,效果完美,散花!
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
