C++ 性能:检查一块内存是不是在特定单元格中具有特定值

Posted

技术标签:

【中文标题】C++ 性能:检查一块内存是不是在特定单元格中具有特定值【英文标题】:C++ performance: checking a block of memory for having specific values in specific cellsC++ 性能:检查一块内存是否在特定单元格中具有特定值 【发布时间】:2011-06-25 08:21:15 【问题描述】:

我正在研究二维装箱算法。我已经向similar question 询问了有关 php 性能的问题——打包太慢了——现在代码已转换为 C++。

它仍然很慢。因此,我的程序所做的是分配动态内存块并用字符“o”填充它们

char* bin;
bin = new (nothrow) char[area];
if (bin == 0) 
    cout << "Error: " << area << " bytes could not be allocated";
    return false;

for (int i=0; i<area; i++) 
    bin[i]='o';

(对于我的数据集,它们的大小在 1kb 到 30kb 之间)

然后程序会检查当前内存块中“x”字符的不同组合。

void place(char* bin, int* best, int width)
   
    for (int i=best[0]; i<best[0]+best[1]; i++)
        for (int j=best[2]; j<best[2]+best[3]; j++)
            bin[i*width+j] = 'x';

检查不重叠的函数之一在运行时被调用数百万次。

bool fits(char* bin, int* pos, int width)
   
    for (int i=pos[0]; i<pos[0]+pos[1]; i++)
        for (int j=pos[2]; j<pos[2]+pos[3]; j++)
            if (bin[i*width+j] == 'x')
                return false;
    return true;

所有其他的东西只占运行时间的百分之一,所以我需要让这两个家伙(适合和放置)更快。谁是罪魁祸首?

由于我只有两个选项“x”和“o”,我可以尝试只使用一位而不是 char 占用的整个字节。但我更关心速度,你认为它会使事情变得更快吗?

谢谢!

更新:按照 MSalters 的建议,我将 int* pos 替换为 rect posbest 相同)。起初我看到了改进,但我用更大的数据集进行了更多测试,它似乎恢复了正常的运行时。我会尝试其他建议的技术,并会及时通知您。

更新:使用 memsetmemchr 加速了两次。将 'x' 和 'o' 替换为 '\1' 和 '\0' 并没有显示出任何改进。 __restrict 也没有帮助。总的来说,我现在对程序的性能感到满意,因为我还对算法本身进行了一些改进。我还没有尝试使用位图并使用 -02 (-03) 进行编译...再次感谢大家。

【问题讨论】:

您所在区域的宽度和高度是多少?您通常需要放入多少块? 它可能不会对性能产生太大影响,但无论如何都值得一试:将bestpos的类型更改为const int*,这样编译器就可以知道它可以提升东西就像best[0]+best[1] 一样。但是,即使这是一种改进,也将是非常微小的。 如果bestconst int*,那只意味着best[0] 不能通过 best 更改。由于bin 可以别名best,因此bin[i*width+j] = 'x' 可能会更改best[0]。编译器每次都必须重新计算表达式。手动提升机将解决此问题。 我偷偷怀疑int* bestint* pos 真的应该有struct rect int top; int height; int left; int width; ; 类型。这也会阻止编译器做出悲观的别名假设。 @MSalters:你是对的。让我解决这个问题。 【参考方案1】:

最好的方法是使用复杂度更高的算法。

但即使是您当前的算法也可以加快速度。尝试使用 SSE 指令一次测试约 16 个字节,也可以进行单个大分配并自己拆分,这将比使用库分配器更快(库分配器的优点是可以让您单独释放块,但我不认为你需要那个功能)。

【讨论】:

我单独删除它们,否则我需要提前分配兆字节......我不知道我可能需要多少。我需要谷歌“sse instructions to test ~16 bytes at once”,不知道这是什么意思。【参考方案2】:

[当然:分析它!]

首先使用位而不是字节不会更快。

但是,考虑到字符,您可以将 4 或 8 个字节的块转换为无符号 32 位或 64 位整数(确保您处理对齐),并将其与 'oooo' 或 'oooooooo' 的值进行比较块。这样可以进行非常快速的比较。

现在已经采用整数方法,您可以看到您可以使用位方法执行相同的操作,并在一次比较中处理 64 位。那肯定会真正加快速度。

【讨论】:

【参考方案3】:

位图也会提高速度,因为它们涉及的内存更少,因此会导致更多的内存引用来自缓存。此外,在place 中,您可能希望将best 的元素复制到局部变量中,以便编译器知道您对bin 的写入不会改变best。如果您的编译器支持restrict 的某些拼写,您可能也想使用它。您也可以将place 中的内循环替换为memset 库函数,将fits 中的内循环替换为memchr;不过,这些可能不会带来很大的性能改进。

【讨论】:

他可以找到使用 SSE 指令的 memsetmemchr 的实现,这可以提供相当大的加速。 是的,但我实际上并不知道宽度和高度是多少。如果其中一个很小( 宽度通常大于 128,高度有时会更大。【参考方案4】:

首先,你记得告诉你的编译器进行优化吗?

并关闭慢速数组索引边界检查等?

完成后,通过将二进制值表示为单个位,您将获得显着的加速,因为您可以一次设置或清除 32 位或 64 位。

此外,我倾向于假设动态分配会产生相当多的开销,但显然您已经测量并发现事实并非如此。但是,如果内存管理实际上对时间有很大贡献,那么解决方案在某种程度上取决于使用模式。但是您的代码可能会生成类似堆栈的分配/释放行为,在这种情况下,您可以将分配优化到几乎没有;只需在开始时分配一大块内存,然后从那里子分配堆栈。

考虑到您当前的代码:

void place(char* bin, int* best, int width)
   
    for (int i=best[0]; i<best[0]+best[1]; i++)
        for (int j=best[2]; j<best[2]+best[3]; j++)
            bin[i*width+j] = 'x';

由于可能的别名,编译器可能没有意识到,例如best[0] 在循环期间将保持不变。

那么,告诉它:

void place(char* bin, int const* best, int const width)

    int const maxY = best[0] + best[1];
    int const maxX = best[2] + best[3];

    for( int y = best[0]; y < maxY; ++y )
    
        for( int x = best[2]; x < maxX; ++x )
        
            bin[y*width + x] = 'x';
        
    

您的编译器很可能会将 y*width 计算提升出内部循环,但为什么不告诉它也这样做:

void place(char* bin, int* best, int const width)

    int const maxY = best[0]+best[1];
    int const maxX = best[2]+best[3];

    for( int y = best[0]; y < maxY; ++y )
    
        int const startOfRow  = y*width;

        for( int x = best[2]; x < maxX; ++x )
        
            bin[startOfRow + x] = 'x';
        
    

这种手动优化(也适用于其他例程)可能有帮助,也可能没有帮助,这取决于您的编译器有多聪明。

接下来,如果这还不够,可以考虑用std::fill(或memset)替换内部循环,一口气完成一整行。

如果这没有帮助或没有足够的帮助,请切换到位级表示。

也许值得一提并尝试一下,每台 PC 都内置了用于优化位级操作的硬件支持,即图形加速卡(过去称为 blitter 芯片)。因此,您可能只使用图像库和黑白位图。但由于您的矩形很小,我不确定设置开销是否会超过实际操作的速度——需要测量。 ;-)

干杯,

【讨论】:

看起来我的编译器会自动提升。我使用 Xcode...所以它可能是 gcc 或 g++,对吧? 不,实际上手动吊装它的工作速度快 5-10%。我假设您错过了 int const* 中最后一段摘录中的 const 关键字? 如何告诉编译器进行优化?并关闭慢速数组索引边界检查等?谢谢! @dfo:这取决于编译器,而您如何告诉 IDE 反过来告诉编译器取决于 IDE。但我记得,以 g++ 为例,选项-O2(或-O3)开启了一些优化。并且使用 Visual c++,选项/O2 请求速度优化。关闭愚蠢的检查主要是 Visual C++ 的事情。很抱歉我没有找到,但这是一些预处理器宏的问题,可能还有一些编译器选项可以关闭检查。【参考方案5】:

我期望的最大改进来自一个不平凡的改变:

// changed pos to class rect for cleaner syntax
bool fits(char* bin, rect pos, int width)

    if (bin[pos.top()*width+pos.left()] == 'x')
                return false;
    if (bin[(pos.bottom()-1*width+pos.right()] == 'x')
                return false;
    if (bin[(pos.bottom()*width+pos.left()] == 'x')
                return false;
    if (bin[pos.top()*width+pos.right()] == 'x')
                return false;

    for (int i=pos.top(); i<=pos.bottom(); i++)
        for (int j=pos.left(); j<=pos.right(); j++)
            if (bin[i*width+j] == 'x')
                return false;
    return true;

当然,您正在测试bin[(pos.bottom()-1*width+pos.right()] 两次。但是你第一次这样做是在算法的早期。您添加框,这意味着相邻的 bin 之间存在很强的相关性。因此,通过首先检查角落,您通常会更早返回。您甚至可以考虑在中间添加第五张支票。

【讨论】:

我在调用这个函数之前确实检查了左上角,但我没有考虑过检查其他角。让我试试。 检查拐角两次似乎会使事情变慢一点。至少在我的测试中。 如果rect pos 相当小,这很有可能。在 2x2 矩形的最终情况下,这显然根本不是改进。【参考方案6】:

除了关于使用分析器的强制性声明之外, 上面关于用位图替换东西的建议是一个非常好的主意。如果这对你没有吸引力..

考虑更换

for (int i=0; i<area; i++) 
    bin[i]='o';

memset(bin, 'o', area);

通常,memset 会更快,因为它编译成的机器代码更少。

还有

void place(char* bin, int* best, int width)
   
    for (int i=best[0]; i<best[0]+best[1]; i++)
        for (int j=best[2]; j<best[2]+best[3]; j++)
            bin[i*width+j] = 'x';

还有一点空间。需要改进

void place(char* bin, int* best, int width)
   
    for (int i=best[0]; i<best[0]+best[1]; i++)

        memset(                         (i * width)  + best[2], 
                'x', 
                (best[2] + best[3]) - (((i * width)) + best[2]) + 1); 

通过消除其中一个循环。

最后一个想法是更改数据表示。 考虑使用 '\0' 字符代替 'o' 和 '\1' 代替 'x' 字符。这有点像使用位图。

这将使您能够像这样进行测试。

if (best[1])

    // Is a 'x'

else

    // Is a 'o'

这可能会产生更快的代码。探查器再次成为您的朋友 :)

这种表示法还可以让您简单地对一组字符求和,以确定有多少个 'x' 和 'o'。

int sum = 0;
for (int i = 0; i < 12; i++)

    sum += best[i];


cout << "There are " << sum << "'x's in the range" << endl;

祝你好运

邪恶。

【讨论】:

memset 帮助了,谢谢。 memchr 帮助更多,它可能比在循环中添加 '\1' 更快。【参考方案7】:

如果您的基本类型有 2 个值,我会首先尝试使用 bool。然后编译器知道你有 2 个值,并且可能能够更好地优化一些东西。 Appart 在可能的情况下添加 const(例如 fit(bool const*,...) 的参数)。

【讨论】:

【参考方案8】:

我会考虑内存缓存中断。这些函数通过更大矩阵内的子矩阵运行——我想宽度和高度都要大很多倍。 这意味着小矩阵行是连续的内存,但在行之间它可能会破坏内存缓存页面。 考虑以使子矩阵元素尽可能彼此靠近的顺序表示内存中的大矩阵单元。那不是保持一个连续的全线向量。我想到的第一个选择是将你的大矩阵递归地分解为大小为 [ 2^i, 2^i ] 有序 top-left, top-right, bottom-left, bottom-right 的矩阵。

1) 即如果您的矩阵大小为 [X,Y],以大小为 X*Y 的数组表示,则元素 [x,y] 位于数组中的位置(x,y):

用 (y*X+x) 代替:

unsigned position( rx, ry )

  unsigned x = rx;
  unsigned y = rx;
  unsigned part = 1;
  unsigned pos = 0;
  while( ( x != 0 ) && ( y != 0 ) ) 
    unsigned const lowest_bit_x = ( x % 2 );
    unsigned const lowest_bit_y = ( y % 2 );
    pos += ( ((2*lowest_bit_y) + lowest_bit_x) * part );
    x /= 2; //throw away lowest bit
    y /= 2;
    part *= 4; //size grows by sqare(2)
  
  return pos;

我没有检查这段代码,只是为了解释我的意思。 如果需要,也可以尝试寻找更快的方法来实现。

但请注意,您分配的数组将大于 X*Y,它必须尽可能小 (2^(2*k)),除非 X 和 Y 的大小比例大致相同,否则这将是一种浪费。但是可以通过先将大矩阵进一步分解为正方形来解决。

然后缓存的好处可能会超过更复杂的位置(x,y)。

2) 然后尝试在 fit() 和 place() 中找到遍历子矩阵元素的最佳方法。还不确定它是什么,不一定像你现在做的那样。基本上,大小为 [x,y] 的子矩阵应该分成不超过 y*log(x)*log(y) 的块,这些块在数组表示中是连续的,但它们都适合不超过 4 个大小的块4*x*y。所以最后,对于小于内存缓存页面的矩阵,您将获得不超过 4 次内存缓存中断,而您的原始代码可能会中断 y 次。

【讨论】:

以上是关于C++ 性能:检查一块内存是不是在特定单元格中具有特定值的主要内容,如果未能解决你的问题,请参考以下文章

检查单元格中的特定字母或一组字母

如何从具有多个数组的字典中获取特定键并存储到放置在表格视图单元格中的字符串中

Swift:Tableview 中特定单元格中的 addSublayer 不是每个单元格

一个管理多维数组的类!如何管理单元格中的不同数据类型?

检查datagridview单元格中的数据是不是为null

使用excel vba将特定文本行保留在单元格中以获取特定起始字母