簡體   English   中英

如何使這個遞歸 function 更快? (四叉樹)

[英]How do I make this recursive function faster? (Quadtree)

我正在學習 C++ 並且正在做一些我在 java 開始時感到舒服的事情。 使用四叉樹進行粒子模擬和植絨以廉價地在區域中查找粒子。 一切正常,但是當我使用四叉樹從一個區域獲取粒子時,它真的很慢(5000 次調用大約需要 1 秒)。

我嘗試用數組替換向量並測量 function 各個部分的執行時間。 我是否犯了任何新手錯誤,例如不必要地復制對象等? 我正在使用 5000 個粒子,我無法想象 1fps 是最快的 go。

根據請求的最小可重現示例的完整代碼:

主文件

#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;
}

四叉樹.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
};

四叉樹.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;
    }
}

粒子.h

#pragma once

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

矩形.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);
};

矩形.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);
}

所以......在原始代碼中創建四叉樹大約需要0.001s秒(相對微不足道),而鄰居搜索大約需要0.06s - 這是我們的罪魁禍首(如 OP 所述)。

std::vector<Particle*> neighbours作為對getCircle function 的引用傳遞,消除了function末尾的insert調用以及新的向量分配(大家好,大家都說“哦,它將被優化掉自動地”)。 時間減少到0.011s

可以將nieghbours向量從主循環中取出,並在使用后清除,以便它僅在第一幀上調整大小。

我沒有看到任何更明顯的目標(沒有進行完全重寫)。 也許我稍后會添加一些東西。


我決定更系統地處理這個問題:我為我所做的每一個更改添加了一個#if開關,並實際記錄了一些統計數據,而不是盯着它。 (每一次變化都是增量添加的,時間包括樹的構建)。

原來的 引用 出圈
分鍾時間: 0.0638s 0.0127s 0.0094s
平均時間: 0.0664s 0.0136s 0.0104s
最長時間: 0.0713s 0.0157s 0.0137s

所有測量均在我的機器上完成,並使用QueryPerfoemanceCounter進行了優化構建。


我確實最終重寫了整個事情......

擺脫了向量。

  • Quadtree::particles現在是帶有計數的Particle* particles[capacity]
  • sections是一個指針; isSplit只檢查sections是否為0
  • 由於已知粒子的總數(或最大),因此getCircle可以返回的粒子數不能超過此數。 所以我在主循環之外分配了那么多來存儲neighbours 添加另一個結果只涉及碰撞指針(甚至沒有簽入版本)。 並在使用后通過將計數設置為 0 來重置它(請參閱 arena 或凹凸分配器)。
  • 從粒子數可以推斷出四叉樹節點的最大數量。 因此,類似地, split ting 只會將指針增加 4。

嘗試在getCircle中預先計算Rect ,或將pxpyr (和/或該 rect )放入結構(作為值或引用傳遞)不會產生任何改進(或有害)。 (由 Goswin von Brederlow 建議)。

然后我翻轉了遞歸(由 Ted Lyngmo 建議)。 臨時堆棧再次被預先分配。 然后我對insert做了同樣的事情。

改寫 非遞歸的 也插入
min_time: 0.0077 0.0069 0.0068
平均時間: 0.0089 0.0073 0.0070
最大時間: 0.0084 0.0078 0.0074

所以最后最有影響力的事情是第一個 - 不是每次調用都insert和創建不必要的向量,而是通過引用傳遞相同的向量。

最后一件事 - 可能想要單獨存儲四叉樹粒子,因為大多數時候getCircle是遍歷不存儲粒子的節點。

否則,我看不到如何改進這一點了。 在這一點上,它需要一個真正聰明或瘋狂的人......

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM