簡體   English   中英

計算包含高維向量的兩個矩陣之間的最小歐氏距離的最快方法

[英]Fastest way to calculate minimum euclidean distance between two matrices containing high dimensional vectors

我在另一個主題上開始了類似的問題,但后來我專注於如何使用OpenCV。 由於未能實現我原先想要的東西,我會在這里問到我想要的東西。

我有兩個矩陣。 矩陣a為2782x128,矩陣b為4000x128,均為無符號字符值。 值存儲在單個數組中。 對於a中的每個向量,我需要b中具有最接近的歐氏距離的向量的索引。

好的,現在我的代碼實現了這個:

#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <cstdio>
#include <math.h>
#include <time.h>
#include <sys/timeb.h>
#include <iostream>
#include <fstream>
#include "main.h"

using namespace std;

void main(int argc, char* argv[])
{
    int a_size;
    unsigned char* a = NULL;
    read_matrix(&a, a_size,"matrixa");
    int b_size;
    unsigned char* b = NULL;
    read_matrix(&b, b_size,"matrixb");

    LARGE_INTEGER liStart;
    LARGE_INTEGER liEnd;
    LARGE_INTEGER liPerfFreq;
    QueryPerformanceFrequency( &liPerfFreq );
    QueryPerformanceCounter( &liStart );

    int* indexes = NULL;
    min_distance_loop(&indexes, b, b_size, a, a_size);

    QueryPerformanceCounter( &liEnd );

    cout << "loop time: " << (liEnd.QuadPart - liStart.QuadPart) / long double(liPerfFreq.QuadPart) << "s." << endl;

    if (a)
    delete[]a;
if (b)
    delete[]b;
if (indexes)
    delete[]indexes;
    return;
}

void read_matrix(unsigned char** matrix, int& matrix_size, char* matrixPath)
{
    ofstream myfile;
    float f;
    FILE * pFile;
    pFile = fopen (matrixPath,"r");
    fscanf (pFile, "%d", &matrix_size);
    *matrix = new unsigned char[matrix_size*128];

    for (int i=0; i<matrix_size*128; ++i)
    {
        unsigned int matPtr;
        fscanf (pFile, "%u", &matPtr);
        matrix[i]=(unsigned char)matPtr;
    }
    fclose (pFile);
}

void min_distance_loop(int** indexes, unsigned char* b, int b_size, unsigned char* a, int a_size)
{
    const int descrSize = 128;

    *indexes = (int*)malloc(a_size*sizeof(int));
    int dataIndex=0;
    int vocIndex=0;
    int min_distance;
    int distance;
    int multiply;

    unsigned char* dataPtr;
    unsigned char* vocPtr;
    for (int i=0; i<a_size; ++i)
    {
        min_distance = LONG_MAX;
        for (int j=0; j<b_size; ++j)
        {
            distance=0;
            dataPtr = &a[dataIndex];
            vocPtr = &b[vocIndex];

            for (int k=0; k<descrSize; ++k)
            {
                multiply = *dataPtr++-*vocPtr++;
                distance += multiply*multiply;
                // If the distance is greater than the previously calculated, exit
                if (distance>min_distance)
                    break;
            }

            // if distance smaller
            if (distance<min_distance)
            {
                min_distance = distance;
                (*indexes)[i] = j;
            }
            vocIndex+=descrSize;
        }
        dataIndex+=descrSize;
        vocIndex=0;
    }
}

附帶的是帶有樣本矩陣的文件。

matrixa matrixb

我正在使用windows.h來計算消耗時間,所以如果你想在另一個平台上測試代碼而不是windows,只需更改windows.h標題並改變計算消耗時間的方式。

我的電腦中的這段代碼約為0.5秒。 問題是我在Matlab中有另一個代碼在0.05秒內完成同樣的事情。 在我的實驗中,我每秒都會收到幾個像矩陣一樣的矩陣,所以0.5秒就太多了。

現在用matlab代碼來計算:

aa=sum(a.*a,2); bb=sum(b.*b,2); ab=a*b'; 
d = sqrt(abs(repmat(aa,[1 size(bb,1)]) + repmat(bb',[size(aa,1) 1]) - 2*ab));
[minz index]=min(d,[],2);

好。 Matlab代碼使用的是(xa)^ 2 = x ^ 2 + a ^ 2 - 2ab。

所以我的下一次嘗試是做同樣的事情。 我刪除了自己的代碼進行相同的計算,但是大約是1.2秒。

然后,我嘗試使用不同的外部庫。 第一次嘗試是Eigen:

const int descrSize = 128;
MatrixXi a(a_size, descrSize);
MatrixXi b(b_size, descrSize);
MatrixXi ab(a_size, b_size);

unsigned char* dataPtr = matrixa;
for (int i=0; i<nframes; ++i)
{
    for (int j=0; j<descrSize; ++j)
    {
        a(i,j)=(int)*dataPtr++;
    }
}
unsigned char* vocPtr = matrixb;
for (int i=0; i<vocabulary_size; ++i)
{
    for (int j=0; j<descrSize; ++j)
    {
        b(i,j)=(int)*vocPtr ++;
    }
}
ab = a*b.transpose();
a.cwiseProduct(a);
b.cwiseProduct(b);
MatrixXi aa = a.rowwise().sum();
MatrixXi bb = b.rowwise().sum();
MatrixXi d = (aa.replicate(1,vocabulary_size) + bb.transpose().replicate(nframes,1) - 2*ab).cwiseAbs2();

int* index = NULL;
index = (int*)malloc(nframes*sizeof(int));
for (int i=0; i<nframes; ++i)
{
    d.row(i).minCoeff(&index[i]);
}

這個特征代碼的成本約為1.2,表示:ab = a * b.transpose();

使用opencv的類似代碼也被使用,並且ab = a * b.transpose()的成本; 是0.65秒。

所以,matlab能夠如此快速地完成同樣的事情並且我無法使用C ++真的很煩人! 當然能夠運行我的實驗會很棒,但我認為缺乏知識真的讓我煩惱。 如何實現至少與Matlab相同的性能? 任何類型的溶解都是受歡迎的。 我的意思是,任何外部庫(如果可能的話免費),循環展開東西,模板東西,SSE intructions(我知道它們存在),緩存東西。 正如我所說,我的主要目的是增加我的知識,因為能夠以更快的速度編寫這樣的代碼。

提前致謝

編輯:David Hammen建議的更多代碼。 在進行任何計算之前,我將數組轉換為int。 這是代碼:

void min_distance_loop(int** indexes, unsigned char* b, int b_size, unsigned char* a, int a_size)
{
    const int descrSize = 128;

    int* a_int;
    int* b_int;

    LARGE_INTEGER liStart;
    LARGE_INTEGER liEnd;
    LARGE_INTEGER liPerfFreq;
    QueryPerformanceFrequency( &liPerfFreq );
    QueryPerformanceCounter( &liStart );

    a_int = (int*)malloc(a_size*descrSize*sizeof(int));
    b_int = (int*)malloc(b_size*descrSize*sizeof(int));

    for(int i=0; i<descrSize*a_size; ++i)
        a_int[i]=(int)a[i];
    for(int i=0; i<descrSize*b_size; ++i)
        b_int[i]=(int)b[i];

    QueryPerformanceCounter( &liEnd );

    cout << "Casting time: " << (liEnd.QuadPart - liStart.QuadPart) / long double(liPerfFreq.QuadPart) << "s." << endl;

    *indexes = (int*)malloc(a_size*sizeof(int));
    int dataIndex=0;
    int vocIndex=0;
    int min_distance;
    int distance;
    int multiply;

    /*unsigned char* dataPtr;
    unsigned char* vocPtr;*/
    int* dataPtr;
    int* vocPtr;
    for (int i=0; i<a_size; ++i)
    {
        min_distance = LONG_MAX;
        for (int j=0; j<b_size; ++j)
        {
            distance=0;
            dataPtr = &a_int[dataIndex];
            vocPtr = &b_int[vocIndex];

            for (int k=0; k<descrSize; ++k)
            {
                multiply = *dataPtr++-*vocPtr++;
                distance += multiply*multiply;
                // If the distance is greater than the previously calculated, exit
                if (distance>min_distance)
                    break;
            }

            // if distance smaller
            if (distance<min_distance)
            {
                min_distance = distance;
                (*indexes)[i] = j;
            }
            vocIndex+=descrSize;
        }
        dataIndex+=descrSize;
        vocIndex=0;
    }
}

現在整個過程為0.6,開始時的鑄造循環為0.001秒。 也許我做錯了什么?

EDIT2:關於Eigen的一切? 當我尋找外部文庫時,他們總是談論Eigen及其速度。 我做錯了什么? 這里使用Eigen的簡單代碼顯示它不是那么快。 也許我錯過了一些配置或一些旗幟,或者......

MatrixXd A = MatrixXd::Random(1000, 1000);
MatrixXd B = MatrixXd::Random(1000, 500);
MatrixXd X;

這段代碼約為0.9秒。

正如您所觀察到的,您的代碼由代表大約2.8e9算術運算的矩陣產品支配。 Yopu說Matlab(或者更確切地說是高度優化的MKL)在大約0.05秒內計算它。 這表示57 GFLOPS的速率表明它不僅使用矢量化而且還使用多線程。 使用Eigen,您可以通過在啟用OpenMP的情況下進行編譯來啟用多線程( -fopenmp with gcc)。 在我5歲的計算機(2.66Ghz Core2)上,使用浮點數和4個線程,你的產品需要大約0.053s,而沒有OpenMP的0.16s,所以編譯標志一定有問題。 總結一下,要獲得Eigen的最佳效果:

  • 以64位模式編譯
  • 使用花車(由於矢量化,雙倍速度慢兩倍)
  • 啟用OpenMP
  • 如果您的CPU具有超線程,則要么禁用它,要么將OMP_NUM_THREADS環境變量定義為物理內核的數量(這非常重要,否則性能會非常糟糕!)
  • 如果您正在運行其他任務,那么將OMP_NUM_THREADS減少為nb_cores-1可能是個好主意
  • 使用最新的編譯器,GCC,clang和ICC最好,MSVC通常較慢。

在你的C ++代碼中,有一件事肯定會讓你感到傷心的是它有一大堆char到int轉換。 通過boatload,我的意思是最多2 * 2782 * 4000 * 128 char到int轉換。 那些 charint轉換的速度很慢,非常慢。

您可以通過分配一對 int數組(一個2782 * 128和另一個4000 * 128)來減少這個轉換為(2782 + 4000)* 128這樣的轉換,以包含 char* achar* b的轉換為整數的內容 char* b陣列。 使用這些 int*數組而不是 char*數組。

另一個問題可能是你使用 intlong 我不在Windows上工作,所以這可能不適用。 在我工作的機器上, int是32位, long現在是64位。 32位是綽綽有余,因為255 * 255 * 128 <256 * 256 * 128 = 2 23

這顯然不是問題。

令人驚訝的是,有問題的代碼並沒有計算出Matlab代碼正在創建的巨大的2728 x 4000陣列。 更令人驚訝的是Matlab最有可能用雙打而不是整數來做這件事 - 而且它仍然在擊敗C / C ++代碼。

一個大問題是緩存。 4000 * 128陣列對於1級緩存來說太大了,而且你在2782次迭代這個大陣列。 你的代碼在內存上做得太多了。 要解決此問題,請使用b數組的較小塊,以便您的代碼盡可能長時間使用1級緩存。

另一個問題是優化if (distance>min_distance) break; 我懷疑這實際上是一種不優化。 if在最里面的循環中進行測試通常是一個壞主意。 盡可能快地沖擊內部產品。 除了浪費的計算,擺脫這個測試是沒有害處的。 有時最好做出明顯不需要的計算,如果這樣做可以刪除最內層循環中的分支。 這是其中一個案例。 您可以通過取消此測試來解決您的問題。 試着這樣做。

回到緩存問題,您需要擺脫這個分支,以便您可以將ab矩陣上的操作拆分成更小的塊,一次不超過256行的塊。 這就是128個無符號字符的行數適合兩個現代英特爾芯片的L1緩存中的一個。 由於250除以4000,因此從邏輯上將b矩陣拆分為16個塊。 您可能希望形成大型2872乘4000內部產品,但是以小塊形式進行。 你可以添加if (distance>min_distance) break; 返回,但是在塊級而不是逐字節級別執行此操作。

你應該能夠擊敗Matlab,因為它幾乎肯定會使用雙打,但你可以使用無符號的字符和整數。

矩陣乘法通常使用兩個矩陣之一的最差可能的高速緩存訪​​問模式,並且解決方案是轉置其中一個矩陣並使用專用的乘法算法來處理以這種方式存儲的數據。

您的矩陣已經存儲轉置。 通過將其轉換為正常順序,然后使用正常矩陣乘法,您的絕對殺戮性能。

編寫自己的矩陣乘法循環,將索引的順序反轉到第二個矩陣(具有轉置它的效果,而不實際移動任何東西並破壞緩存行為)。 並為您的編譯器傳遞任何用於啟用自動向量化的選項。

暫無
暫無

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

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