表现与正确/偏好?

时间:2010-10-16 23:48:45

标签: c++ optimization

typedef unsigned char uChar;
typedef signed char sChar;

typedef unsigned short uShort;
typedef signed short sShort;

typedef unsigned int uInt;
typedef signed int sInt;

typedef unsigned long uLong;
typedef signed long sLong;

我有一个typedef列表,所以当我定义变量时,我可以准确。例如,如果我只需要数字0-5,我就会使用uChar。但我正在使用C ++并正在制作引擎。我正在阅读关于.NET占用X字节的布尔值,并且由于内存对齐,它可以更快地使用整数。

由于内存对齐,性能等原因,是否有理由使用int而不是uChar?

6 个答案:

答案 0 :(得分:13)

这种过早优化很少发生。我会选择一个数据结构并继续使用它。一旦您有一个有问题的完整系统,请对其进行分析以找出问题所在。你猜测和击中头部表现不佳的机会确实很小。

答案 1 :(得分:10)

  • 那些typedef并不比它们命名的更准确。它们更像简洁,但非标准。
  • 如果您想更精确,请使用#include <stdint.h>获取int8_tuint32_t等。
  • 如果您需要担心内存对齐,可以通过其他方式找到。
  • 如果您需要存储大量布尔值,请查看std::bitsetstd::vector<bool>
  • 如果您需要存储一个布尔值,请使用bool

答案 2 :(得分:5)

你真的不想在没有关键路径的东西上浪费时间。一旦你有了工作,那么你应该剖析并查看问题所在。然后你可以加快麻烦点。

快速但不起作用的系统毫无价值。一个有效的慢速系统对某些人来说很有用,并且随着它变得更快而变得更有用。

还要记住,一个未经优化但适当的算法几乎每次都会击败超级优化的差算法。

答案 3 :(得分:1)

在开始考虑微优化性能之前,模拟原型并在其上使用分析器。请记住:如果它是一个常数(或者甚至是对系数的微小变化),Big-O也会对它进行相同的处理。

根据我的经验,使用无符号类型会破坏许多常见的错误检查方法,并使它几乎立即遇到包装错误(和错误)的整数存储,但同时又使得更难以推理关于解决方案。

此外,使用无符号类型时,隐式强制转换更容易产生错误。

例如:

#include<iostream>

void SomeFunction(uint32_t value)
{
  if(value < 0)
  {
    // unreachable code.  What do we do instead?
    throw std::runtime_error("value must be non-negative");
  }
}

uint32_t SomeOtherFunction()
{
  return (uint32_t)2000000000 + (uint32_t)2000000000;
}

int main(int argc, char* argv[])
{
  int someValue = -1;
  SomeFunction(someValue);
  someValue = SomeOtherFunction();
  std::cout << someValue;
}
  

-294967296

答案 4 :(得分:1)

因为C标准明确定义了超出无符号类型边界的影响,所以编译器可能必须添加额外的代码以使它们按照指示行事。因此,对于内存中的内容,一种数据类型可能更快,而对于保存在寄存器中的内容,另一种类型更快。例如,考虑代码:

  uInt16 var1;
  Int32 var2;

  var1++;
  var2 = var1;

我使用的ARM处理器只有32位指令用于寄存器操作,但可以执行8位,16位和32位加载和存储。如果var1在内存中,它就可以像32位整数一样操作,但是如果它在寄存器中,编译器必须添加一条指令来清除高位字,然后再将它复制到var2。如果var1是一个带符号的16位整数,那么从内存加载它会比无符号(因为必要的符号扩展)慢,但如果它保存在寄存器中,编译器就不需要担心上层位。

答案 5 :(得分:1)

是的,使用int而不是char通常会导致明显的性能免费赠品。因此,使用C语言的int来尝试匹配处理器中寄存器的本机大小。

最好尽可能使用无符号整数,并且只在罕见/特定情况下,使用unsigned int以外的东西。除非你有充分的理由,否则避免使用小于int的东西。如果你试图使用小于int的项目来从编程习惯中获得一些免费赠品,那么你需要以另一种方式改变你的习惯。同样适用于未签名的,使用未签名的任何内容,除非您有充分的理由使用签名。

基本上反汇编(或编译为asm),查看您最喜欢的和其他编译器生成的内容,注意由字符引起的未对齐寻址,注意高位的屏蔽,签名字符的符号扩展等。这些是有时是免费的,有时不依赖于该字节来自和去往的平台。也尝试至少x86和arm,也许是mips,gcc 3.x,4.x和llvm。特别注意单个char如何与一行声明中的int列表混合可能会导致后面的int不对齐,从地址的角度来看这对于x86来说是好的,但是会降低性能(在x86甚至是缓存)。首先放置对齐的变量,然后放置未对齐的变量。其他不能或不愿意进行未对齐访问的平台会浪费额外的字节作为填充,因此您不一定要节省内存。过早优化试图调整到可变长度。使用简单的习惯,比如使用无符号整数来处理所有事情,除非你有一个特定的原因,将较大的,对齐的变量和结构放在声明列表中,并且未对齐的东西最后(短路然后是字符)。

乘法(和除法)使这种习惯变得丑陋,避免乘法和代码中的分歧是最好的习惯。如果你必须使用一个非常了解它的实现。例如,将两个字符相乘而不是两个整数(如果数字支持它)要好得多,所以如果您确实知道整数是7位或5位或任何数量,请将它们拼写为乘法并允许硬件乘法发生而不是软乘法。 (如果这些可变大小改变!!可能是休眠虫)。即使许多处理器具有硬件倍增,但实际上可以直接使用它是非常罕见的。除非你帮助编译器,否则它必须进行库调用以检查溢出等,并最终可能导致软乘法,这非常昂贵。划分是不好的,因为大多数处理器不包括鸿沟。如果他们这样做,你可能陷入同样的​​陷阱。乘以N位* N位变为结果的2 * N位,这是乘法问题的来源。除数后,数字保持不变或变小。在这两种情况下,isas都不总是提供足够的位来覆盖溢出,并且需要库调用来解决处理器硬件限制。

浮点是一个类似的故事,只需要小心浮点。除非绝对必要,否则不要使用它。大多数人都不记得了

float a;
float b;
...
b = a * 1.0;
除非另有说明,否则C采用双精度,因此上述乘法需要将a转换为double,然后乘以然后将结果转换回单个。有些fpus可以在同一指令中以时钟为代价进行精度转换,有些则不能。精确转换是大多数浮点处理器错误存在(或确实存在)的地方。因此要么将双打用于所有事情,要么小心编码以避免这些陷阱:

float a;
float b;
...
b = a * 1.0F;

此外,大多数情况下没有FPU,因此避免浮点运算甚至比避免固定点乘法和除法更多。假设大多数fpus都有bug。编写好的浮点代码很困难(程序员通常不知道如何使用它并为其编写代码,从而丢掉了相当多的精度。)

一些简单的习惯和你的代码作为免费赠品显然更快更干净。此外,编译器不必努力工作,因此您可以减少编译器错误。

编辑添加浮点精度示例:

float fun1 ( float a )
{
    return(a*7.1);
}

float fun2 ( float a )
{
    return(a*7.1F);
}

the first function contained:
    mulsd   .LC0(%rip), %xmm0
using a 64 bit floating point constant
.LC0
    .long   1717986918
    .long   1075603046

and the second function contains the desired single precision multiply
    mulss   .LC1(%rip), %xmm0
with a single precision constant
.LC1
    .long   1088631603
char fun1 ( char a )
{
    return(a+7);
}
int fun2 ( int a )
{
    return(a+7);
}
fun1:
    add r0, r0, #7
    and r0, r0, #255
    bx  lr
fun2:
    add r0, r0, #7
    bx  lr