You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化四叉树递归查询函数,提升粒子模拟运行速度?

四叉树粒子查询性能优化问题

我正在学习C++,用熟悉的Java思路开发基于四叉树的粒子集群模拟系统,目的是通过四叉树低成本查询区域内的粒子。目前功能正常,但四叉树查询区域粒子的速度极慢——5000次调用耗时约1秒,5000个粒子跑起来只能到1FPS,完全无法接受。

我试过把vector换成数组,也测试了函数各部分的执行时间,想排查是不是有不必要的对象复制这类新手错误。下面是完整可复现代码:


main.cpp

#include <string>
#include <iostream>
#include <random>
#include <chrono>
#include <thread>
#include <cmath>

#include "Particle.h"
#include "Quadtree.h"

// Clock
using namespace std::chrono;
using namespace std::this_thread;

// Global constants
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int desiredFPS = 30;
const int frameTimeMS = int(1000 / (double)desiredFPS);
const int numberOfParticles = 5000;

// Random number generation
std::random_device dev;
std::mt19937 rng(dev());
std::uniform_real_distribution<> dist(0, 1);

Particle particles[numberOfParticles];
Quadtree quadtree = Quadtree(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

int main(int argc, char* args[])
{
    for (int i = 0; i < numberOfParticles; i++)
    {
        particles[i] = Particle(dist(rng) * SCREEN_WIDTH, dist(rng) * SCREEN_HEIGHT);
    }

    // Clock for making all frames equally long and achieving the desired framerate when possible
    auto lapStartTime = system_clock::now();

    // Main loop
    for (int i = 0; i < 1; i++)
    {
        // Insert the particles into the quadtree
        quadtree = Quadtree(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
        for (int i = 0; i < numberOfParticles; i++)
        {
            quadtree.insert(&particles[i]);
        }

        double neighbourhoodRadius = 40;
        for (int i = 0; i < numberOfParticles; i++)
        {
            // THIS IS THE PART THAT IS SLOW
            std::vector<Particle*> neighbours = quadtree.getCircle(
                particles[i].x,
                particles[i].y,
                neighbourhoodRadius
            );
        }

        // Update clocks
        auto nextFrameTime = lapStartTime + milliseconds(frameTimeMS);
        sleep_until(nextFrameTime);
        lapStartTime = nextFrameTime;
    }
    return 0;
}

Quadtree.h

#pragma once

#include <vector>
#include "Particle.h"
#include "Rect.h"

class Quadtree
{
public:
    const static int capacity = 10; // Capacity of any section
    Quadtree(double px, double py, double width, double height);
    Quadtree(Rect r);
    bool insert(Particle* p); // Add a particle to the tree
    std::vector<Particle*> getCircle(double px, double py, double r);
    int numberOfItems(); // Total amount in the quadtree
private:
    std::vector<Particle*> particles; // Particles stored by this section
    std::vector<Quadtree> sections; // Branches (only if split)
    Rect area; // Region occupied by the quadtree
    bool isSplit() { return sections.size() > 0; }
    void split(); // Split the quadtree into 4 branches
};

Quadtree.cpp

#include <iostream>
#include "Quadtree.h"

Quadtree::Quadtree(double px, double py, double width, double height)
{
    area = Rect(px, py, width, height);
    sections = {};
    particles = {};
}

Quadtree::Quadtree(Rect r)
{
    area = r;
    sections = {};
    particles = {};
}

bool Quadtree::insert(Particle* p)
{
    if (area.intersectPoint(p->x, p->y))
    {
        if (!isSplit() && particles.size() < capacity)
        {
            particles.push_back(p);
        }
        else
        {
            if (!isSplit()) // Capacity is reached and tree is not split yet
            {
                split();
            }

            // That this is a reference is very important!
            // Otherwise a copy of the tree will be modified
            for (Quadtree& s : sections)
            {
                if (s.insert(p))
                {
                    return true;
                }
            }
        }

        return true;
    }
    else
    {
        return false;
    }
}

std::vector<Particle*> Quadtree::getCircle(double px, double py, double r)
{
    std::vector<Particle*> selection = {};
    if (!isSplit())
    {
        // Add all particles from this section that lie within the circle
        for (Particle* p : particles)
        {
            double a = px - p->x;
            double b = py - p->y;
            if (a * a + b * b <= r * r)
            {
                selection.push_back(p);
            }
        }
    }
    else
    {
        // The section is split so add all the particles from the
        // branches together
        for (Quadtree& s : sections)
        {
            // Check if the branch and the circle even have any intersection
            if (s.area.intersectRect(Rect(px - r, py - r, 2 * r, 2 * r)))
            {
                // Get the particles from the branch and add them to selection
                std::vector<Particle*> branchSelection = s.getCircle(px, py, r);
                selection.insert(selection.end(), branchSelection.begin(), branchSelection.end());
            }
        }
    }
    return selection;
}

void Quadtree::split()
{
    sections.push_back(Quadtree(area.getSection(2, 2, 0, 0)));
    sections.push_back(Quadtree(area.getSection(2, 2, 0, 1)));
    sections.push_back(Quadtree(area.getSection(2, 2, 1, 0)));
    sections.push_back(Quadtree(area.getSection(2, 2, 1, 1)));

    std::vector<Particle*> oldParticles{ particles };
    particles.clear();

    for (Particle* p : oldParticles)
    {
        bool success = insert(p);
    }
}

int Quadtree::numberOfItems()
{
    if (!isSplit())
    {
        return particles.size();
    }
    else
    {
        int result = 0;
        for (Quadtree& q : sections)
        {
            result += q.numberOfItems();
        }
        return result;
    }
}

Particle.h

#pragma once

class Particle {
public:
    double x;
    double y;
    Particle(double px, double py) : x(px), y(py) {}
    Particle() = default;
};

Rect.h

#pragma once

class Rect
{
public:
    double x;
    double y;
    double w;
    double h;
    Rect(double px, double py, double width, double height);
    Rect() : x(0), y(0), w(0), h(0) {}
    bool intersectPoint(double px, double py);
    bool intersectRect(Rect r);
    Rect getSection(int rows, int cols, int ix, int iy);
};

Rect.cpp

#include "Rect.h"

Rect::Rect(double px, double py, double width, double height)
{
    x = px;
    y = py;
    w = width;
    h = height;
}

bool Rect::intersectPoint(double px, double py)
{
    return px >= x && px < x + w && py >= y && py < y + h;
}

bool Rect::intersectRect(Rect r)
{
    return x + w >= r.x && y + h >= r.y && x <= r.x + r.w && y <= r.y + r.w;
}

Rect Rect::getSection(int cols, int rows, int ix, int iy)
{
    return Rect(x + ix * w / cols, y + iy * h / rows, w / cols, h / rows);
}

性能问题分析与优化方案

1. 最严重的性能杀手:getCircle的vector频繁复制与分配

当前getCircle每次调用都会创建新的vector,递归调用时还要把分支的vector内容插入到当前vector,这会导致大量的内存分配、复制操作,5000次调用下来开销极大。

解决方法: 改成通过引用传递输出vector,避免每次创建新容器:

// 修改Quadtree.h中的函数声明
void getCircle(double px, double py, double r, std::vector<Particle*>& out);

// 修改Quadtree.cpp中的实现
void Quadtree::getCircle(double px, double py, double r, std::vector<Particle*>& out)
{
    if (!isSplit())
    {
        for (Particle* p : particles)
        {
            double a = px - p->x;
            double b = py - p->y;
            if (a * a + b * b <= r * r)
            {
                out.push_back(p);
            }
        }
    }
    else
    {
        Rect circleBounds(px - r, py - r, 2 * r, 2 * r);
        for (Quadtree& s : sections)
        {
            if (s.area.intersectRect(circleBounds))
            {
                s.getCircle(px, py, r, out);
            }
        }
    }
}

// main.cpp中调用时提前复用一个vector
std::vector<Particle*> neighbours;
for (int i = 0; i < numberOfParticles; i++)
{
    neighbours.clear(); // 清空而非重新创建
    quadtree.getCircle(particles[i].x, particles[i].y, neighbourhoodRadius, neighbours);
    // 处理neighbours...
}

2. Rect类的intersectRect函数存在bug

当前intersectRect的最后一个条件是y <= r.y + r.w,这里应该是y + h <= r.y + r.h,错误的判断会导致不必要的分支查询,增加无效的递归调用。

修复代码:

bool Rect::intersectRect(Rect r)
{
    return x + w >= r.x && y + h >= r.y && x <= r.x + r.w && y + h <= r.y + r.h;
}

3. 避免临时Rect对象的重复创建

在getCircle中每次判断分支是否相交时,都会创建Rect(px - r, py - r, 2 * r, 2 * r),可以提前创建一次,避免重复构造。

4. 四叉树的sections改用指针而非值存储

当前std::vector<Quadtree> sections存储的是四叉树对象,复制和销毁时都会有额外开销,改用std::vector<std::unique_ptr<Quadtree>> sections可以减少对象复制的成本,尤其是split的时候。

修改示例:

// Quadtree.h中的private成员
std::vector<std::unique_ptr<Quadtree>> sections;

// split函数实现
void Quadtree::split()
{
    sections.emplace_back(std::make_unique<Quadtree>(area.getSection(2, 2, 0, 0)));
    sections.emplace_back(std::make_unique<Quadtree>(area.getSection(2, 2, 0, 1)));
    sections.emplace_back(std::make_unique<Quadtree>(area.getSection(2, 2, 1, 0)));
    sections.emplace_back(std::make_unique<Quadtree>(area.getSection(2, 2, 1, 1)));

    std::vector<Particle*> oldParticles{ particles };
    particles.clear();

    for (Particle* p : oldParticles)
    {
        bool success = insert(p);
    }
}

// insert和getCircle中的遍历改成
for (auto& s : sections)
{
    if (s->insert(p)) { return true; }
}

5. 减少main中的四叉树赋值开销

当前quadtree = Quadtree(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);会先创建临时对象,再赋值给全局的quadtree,改成直接调用clear重置(需要给Quadtree加clear方法),或者直接构造新对象替换:

// 替换main中的赋值为:
quadtree = Quadtree{}; // 利用默认构造,或者加clear方法
// 或者:
quadtree.~Quadtree();
new (&quadtree) Quadtree(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

或者更好的方式是把quadtree声明在main函数内,每次循环重新构造,避免全局对象的赋值开销。


内容的提问来源于stack exchange,提问作者Fractal Salamander

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 15:39:14