为什么uint_least16_t比uint_fast16_t快于x86_64中的乘法?

时间:2010-11-07 02:48:47

标签: c x86-64 multiplication unsigned

C标准对uint_fast*_t类型的家族非常不清楚。在gcc-4.4.4 linux x86_64系统上,类型uint_fast16_tuint_fast32_t的大小均为8个字节。但是,8字节数的乘法似乎比4字节数的乘法慢得多。以下代码演示了:

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int
main ()
{
  uint_least16_t p, x;
  int count;

  p = 1;
  for (count = 100000; count != 0; --count)
    for (x = 1; x != 50000; ++x)
      p*= x;

  printf("%"PRIuLEAST16, p);
  return 0;
}

在程序上运行time命令,我得到了

real 0m7.606s
user 0m7.557s
sys  0m0.019s

如果我将类型更改为uint_fast16_t(以及printf修饰符),则时间变为

real 0m12.609s
user 0m12.593s
sys  0m0.009s

那么,如果stdint.h头文件定义uint_fast16_t(以及uint_fast32_t)为4字节类型会不会更好?

5 个答案:

答案 0 :(得分:5)

如果系统尚未定义,则AFAIK编译器仅定义自己的(u)int_(fast/least)XX_t类型版本。这是因为在单个系统上的所有库/二进制文件中同等定义这些类型非常重要。否则,如果不同的编译器会以不同的方式定义这些类型,那么使用CompilerA构建的库可能与使用CompilerB构建的二进制文件具有不同的uint_fast32_t类型,但是这个二进制文件仍然可以链接到库;没有正式标准要求系统的所有可执行代码都必须由同一个编译器构建(实际上在某些系统上,例如Windows,代码已被所有类型的代码编译,这是很常见的。不同的编译器)。如果现在这个二进制文件调用库的一个函数,那么就会破坏!

所以问题是:这里真的是GCC定义uint_fast16_t,还是实际上是Linux(我的意思是这里的内核),甚至可能是标准C Lib(大多数情况下都是glibc)定义了那些类型?因为如果Linux或glibc定义了这些,那么建立在该系统上的GCC别无选择,只能采用它们已经建立的任何约定。对于所有其他可变宽度类型也是如此:charshortintlonglong long;所有这些类型在 C标准中只有最小保证位宽(对于int它实际上是16位,因此在int为32位的平台上,它是已经比标准所要求的要大得多。


除此之外,我实际上想知道你的CPU /编译器/系统有什么问题。在我的系统上,64位乘法与32位乘法同样快。我修改了你的代码以测试16位,32位和64位:

#include <time.h>
#include <stdio.h>
#include <inttypes.h>

#define RUNS 100000

#define TEST(type)                                  \
    static type test ## type ()                     \
    {                                               \
        int count;                                  \
        type p, x;                                  \
                                                    \
        p = 1;                                      \
        for (count = RUNS; count != 0; count--) {   \
            for (x = 1; x != 50000; x++) {          \
                p *= x;                             \
            }                                       \
        }                                           \
        return p;                                   \
    }

TEST(uint16_t)
TEST(uint32_t)
TEST(uint64_t)

#define CLOCK_TO_SEC(clock) ((double)clockTime / CLOCKS_PER_SEC)

#define RUN_TEST(type)                             \
    {                                              \
        clock_t clockTime;                         \
        unsigned long long result;                 \
                                                   \
        clockTime = clock();                       \
        result = test ## type ();                  \
        clockTime = clock() - clockTime;           \
        printf("Test %s took %2.4f s. (%llu)\n",   \
            #type, CLOCK_TO_SEC(clockTime), result \
        );                                         \
    }

int main ()
{
    RUN_TEST(uint16_t)
    RUN_TEST(uint32_t)
    RUN_TEST(uint64_t)
    return 0;
}

使用未经优化的代码(-O0),我得到:

Test uint16_t took 13.6286 s. (0)
Test uint32_t took 12.5881 s. (0)
Test uint64_t took 12.6006 s. (0)

使用优化代码(-O3),我得到:

Test uint16_t took 13.6385 s. (0)
Test uint32_t took 4.5455 s. (0)
Test uint64_t took 4.5382 s. (0)

第二个输出非常有趣。 @R ..在上面的评论中写道:

  

在x86_64上,32位算术不应该比64位慢   算术,期间。

第二个输出显示32/16位算术不能说同样的事情。即使我的x86 CPU本身可以执行16位运算,32位算术在32/64位CPU上也会明显变慢;与其他一些CPU不同,例如PPC,它只能执行32位算术。但是,这似乎只适用于我的CPU上的乘法,当更改代码进行加/减/除时,16和32位之间没有明显的差别。

以上结果来自英特尔酷睿i7(2.66 GHz),但如果有人有兴趣,我也可以在英特尔酷睿2双核处理器(旧一代CPU)和摩托罗拉PowerPC G4上运行此基准测试。

答案 1 :(得分:3)

我认为这样的设计决策并不容易。这取决于很多因素。目前我不认为你的实验是确凿的,见下文。

首先,没有一个单一概念 fast 应该是什么意思。在这里你强调了乘法,这只是一个特定的观点。

然后x86_64是架构而不是处理器。因此,对于该系列中的不同处理器,结果可能会大不相同。我不认为gcc的类型决定取决于为给定处理器优化的特定命令行开关是否合理。

现在回到你的例子。我猜你还看过汇编代码?它是否使用SSE指令来实现您的代码?您是否打开了处理器特定选项,例如-march=native

编辑:我对您的测试程序进行了一些实验,如果我完全保留它,我基本上可以重现您的测量结果。但是修改和玩弄它我更不相信它是决定性的。

例如,如果我也将内循环更改为向下,则汇编程序看起来几乎与之前相同(但使用递减和对0的测试)但执行大约多50%。所以我猜时间在很大程度上取决于您想要基准测试的指令环境,管道停滞等等。您必须在不同的环境中发布指令,对齐问题和矢量化,以便对fast typedef的适当类型做出决定。

答案 2 :(得分:3)

运行时的实际性能是一个非常复杂的主题。有许多因素,包括Ram内存,硬盘,操作系统;还有许多处理器特有的怪癖。但这会给你一个艰难的过程:

<强> N_fastX_t

  • 为处理器有效计算大多数(加法和减法)运算的最佳大小。这是特定于硬件的,其中32位变量是原生的,并且比16位变量更快(因此使用)。 (例如)
  • 因为它在高速缓存行命中方面没有和N_leastX一样受益,所以主要应该在尽可能快地只需要一个变量的情况下使用它。虽然不是在一个大型阵列中(在两者之间切换的最佳状态,但很可能是平台依赖的)
  • 请注意,快速与最小有几个怪癖的情况,主要是乘法和除法。这是特定于平台的。但是,如果大多数操作是添加/减少/或/和。假设快速更快,通常是安全的。 (再次注意CPU缓存和其他怪癖)

<强> N_leastX_t

  • 硬件允许的最小变量,即至少为X大小。 (例如,某些平台无法分配低于4个字节的变量。事实上,大多数BOOL变量至少占用一个字节,而不是一点)
  • 如果不存在硬件支持,可以通过CPU代价高昂的软件仿真来计算。
  • 由于部分操作基础上的部分硬件支持(与快速相比)可能会导致性能下降。
  • HOWEVER :因为它占用较少的可变空间,所以它可能会更频繁地击中缓存行。这在阵列中更为突出。因此会更快(内存成本&gt; CPU成本)有关详细信息,请参阅http://en.wikipedia.org/wiki/CPU_cache

乘法问题?

还要回答为什么较大的fastX变量在乘法中会变慢。原因是乘法的本质。 (类似于你在学校的想法)

http://en.wikipedia.org/wiki/Binary_multiplier

//Assuming 4bit int
   0011 (3 in decimal)
 x 0101 (5 in decimal)
 ======
   0011 ("0011 x 0001")
  0000- ("0011 x 0000")
 0011-- ("0011 x 0001")
0000--- ("0011 x 0000")
=======
   1111 (15 in decimal)

然而,重要的是要知道计算机是一个“逻辑白痴”。虽然对我们人类显而易见的是跳过尾随的零步骤。计算机仍然可以解决它(它更便宜然后有条不紊地检查然后再进行处理)。因此,这会为相同值的较大尺寸变量创建一个怪癖

   //Assuming 8bit int
      0000 0011 (3 in decimal)
    x 0000 0101 (5 in decimal)
    ===========
      0000 0011 ("0011 x 0001")
    0 0000 000- ("0011 x 0000")
   00 0000 11-- ("0011 x 0001")
  000 0000 0--- ("0011 x 0000")
 0000 0000 ---- (And the remainders of zeros)
 -------------- (Will all be worked out)
 ==============
      0000 1111 (15 in decimal)

虽然我没有在乘法过程中垃圾邮件中剩余的0x0添加内容。重要的是要注意计算机将“完成它们”。因此,较大的变量乘法与较小的变量相比需要更长的时间是很自然的。 (因此,尽可能避免乘法和除法总是好的。)

然而,这是第二个怪癖。它可能不适用于所有处理器。请务必注意,所有CPU操作都以CPU周期计算。在每个循环中,如上所述执行数十(或更多)这样的小添加操作。因此,8位加法可能需要与8位乘法相同的时间,等等。由于各种优化和CPU特定的怪癖。

如果它对你有多大关注。请参阅英特尔:http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html


关于CPU与RAM的其他提及

由于CPU已经超越了摩尔定律,比你的DDR3 RAM要快几倍。

这可能导致花费更多时间从ram查找变量然后CPU“计算”它的情况。这在长指针链中最为突出。

因此,大多数处理器上都存在CPU缓存,以减少“RAM查找”时间。它的用途仅限于特定情况(缓存行受益最多)。对于不适合的情况。注意,RAM查找时间> CPU处理时间(不包括乘法/除法/某些怪癖)

答案 3 :(得分:2)

是的,我认为这只是一个错误。不幸的是,你不能在不破坏ABI的情况下修复这样的错误,但这可能并不重要,因为几乎没有人(当然也没有我知道的库函数)实际使用*int_fast*_t类型。

答案 4 :(得分:1)

仅仅因为我对快速整数类型感到好奇,我对一个真实的解析器进行了基准测试,在其语义部分中,它使用整数类型来索引数组和C ++ - 容器。它执行混合操作而不是简单循环,并且大多数程序不依赖于所选择的整数类型。实际上,对于我的特定数据,任何整数类型都可以。所以所有版本都产生相同的输出。

在装配级别有8种情况:大小为4,签名为2。必须将24个ISO C类型名称映射到八种基本类型。正如Jens已经声明的那样,“良好”的映射必须考虑特定的处理器和特定的代码。因此,在实践中,即使编译器编写者应该知道生成的代码,我们也不应期望完美的结果。

对该示例的许多运行进行了平均,使得运行时间的波动范围仅为最小给定数字的约2。对于此特定设置,结果为:

  • int16_t / uint16_tint64_t / uint64_t之间没有运行时间差异。
  • 对于int8_t / uint8_tint32_t / uint32_t,未签名版本的速度要快得多。
  • 无符号版本总是比签名版本小(文本和数据段)。

编译器:g ++ 4.9.1,选项:-O3 mtune = generic -march = x86-64

CPU:Intel™Core™2 Duo E8400 @ 3.00GHz

映射

|    |Integer|                                                                     |
|Sign|Size   | Types                                                               |
|    |[bits] |                                                                     |
|:--:|------:|:-------------------------------------------------------------------:|
| u  |   8   | uint8_t  uint_fast8_t   uint_least8_t                               |
| s  |   8   | int8_t   int_fast8_t    int_least8_t                                |
| u  |  16   | uint16_t uint_least16_t                                             |
| s  |  16   | int16_t  int_least16_t                                              |
| u  |  32   | uint32_t uint_least32_t                                             |
| s  |  32   | int32_t  int_least32_t                                              |
| u  |  64   | uint64_t uint_fast16_t  uint_fast32_t  uint_fast64_t uint_least64_t |
| s  |  64   | int64_t  int_fast16_t   int_fast32_t   int_fast64_t  int_least64_t  |

尺码和时间

|      | Integer |         |       |       |         |
| Sign | Size    |  text   |  data |  bss  | Time    |
|      | [bits]  | [bytes] |[bytes]|[bytes]| [ms]    |
|:----:|--------:|--------:| -----:|------:|--------:|
|  u   |    8    | 1285801 |  3024 |  5704 | 407.61  |
|  s   |    8    | 1285929 |  3032 |  5704 | 412.39  |
|  u   |   16    | 1285833 |  3024 |  5704 | 408.81  |
|  s   |   16    | 1286105 |  3040 |  5704 | 408.80  |
|  u   |   32    | 1285609 |  3024 |  5704 | 406.78  |
|  s   |   32    | 1285921 |  3032 |  5704 | 413.30  |
|  u   |   64    | 1285557 |  3032 |  5704 | 410.12  |
|  s   |   64    | 1285824 |  3048 |  5704 | 410.13  |