计算包含高维向量的两个矩阵之间的最小欧氏距离的最快方法

时间:2012-09-26 08:16:09

标签: c++ performance opencv matrix-multiplication eigen

我在another thread上开始了类似的问题,但后来我专注于如何使用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代码使用的是(x-a)^ 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秒。

3 个答案:

答案 0 :(得分:3)

正如您所观察到的,您的代码由代表大约2.8e9算术运算的矩阵产品支配。 Yopu说Matlab(或者更确切地说是高度优化的MKL)在大约0.05秒内计算它。这表示57 GFLOPS的速率表明它不仅使用矢量化而且还使用多线程。使用Eigen,您可以通过在启用OpenMP(-fopenmp和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通常较慢。

答案 1 :(得分:2)

在您的C ++代码中,一件肯定会让您受到伤害的事情是它有一大堆char转换为int转换。通过boatload,我的意思是最多2 * 2782 * 4000 * 128 char到int转换。那些charint次转化很慢,非常慢。

你可以通过分配一对int数组,一个2782 * 128和另一个4000 * 128来减少这种转换为(2782 + 4000)* 128这样的转换,以包含转换为整数的内容您的char* achar* 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,因为它几乎肯定会使用双打,但你可以使用未签名的字符和整数。

答案 2 :(得分:1)

矩阵乘法通常使用两个矩阵之一的最差可能的高速缓存访​​问模式,解决方案是转置其中一个矩阵并使用专门的乘法算法,该算法对存储的数据进行处理。

您的矩阵已经存储转置。通过将其转换为正常顺序,然后使用正常矩阵乘法,你绝对会杀死性能。

编写自己的矩阵乘法循环,将索引的顺序反转到第二个矩阵(具有转置它的效果,而不实际移动任何东西并破坏缓存行为)。并为您的编译器传递任何用于启用自动向量化的选项。