【问题标题】:When does binary "become" decimal or hex? Is hex "faster" than decimal?二进制什么时候“变成”十进制或十六进制?十六进制比十进制“快”吗?
【发布时间】:2018-02-24 18:57:21
【问题描述】:

一个人很快就学会了命令,比如用 C 语言,形式

printf("%d", x);

printf("%lu", x);

但是对于 x 的二进制表示,不存在 %lu%d 的类似物。

我的问题首先是,为什么会这样,其次,二进制表示在什么时候 - 在哪个抽象级别 - 会变成十进制还是十六进制?

Stack 网络上的相似点似乎只能引出特定语言的答案或实现/库建议。然而,我的问题是关于我对如何抽象数据以及操作系统是否曾经看到二进制文件的总体理解,或者是否以某种方式甚至比操作系统更低级别的东西覆盖它。

为了进一步强调我前进的方向,考虑一个切题的问题:以十六进制编程源文件是否比以十进制编程(例如在常量或变量初始化期间)对性能(速度或存储)有任何好处?

【问题讨论】:

  • 没有二进制格式说明符,因为几乎不需要以二进制表示形式向人类显示数值。
  • 机器中的0s 和1s 具有在上下文中的含义。例如,8 位二进制值10000000 如果表示无符号整数值,则为十进制值128,如果表示有符号整数值,则为-128
  • ... 在机器级别,使用上下文的一种方式是处理器标记程序员(或编译器)在算术运算后测试。结果设置了几个标志,但根据上下文测试不同的标志。
  • 您将字符表示与标量数值混淆了。
  • 关于为什么二进制的printf 中没有%b 的问题,请参阅this questionthis question

标签: c binary number-formatting cpu-architecture low-level


【解决方案1】:

在哪个抽象级别 - 二进制表示会变成十进制还是十六进制?

根本没有。 CPU 只能看到 0 和 1 的序列。当他们在 组合在一起,这个 0 和 1 可以有一个含义,例如一个序列 32 个 0 和 1 代表一个 32 位整数值。

我们人类不擅长查看 32 个字符并计算我们的值 头,这就是我们使用十进制、八进制、十六进制表示的原因,因为 它更容易处理。标量值 18 是一个不会改变的值,但是 它的表示可能会根据您拥有的位数而改变。 18 英寸 二进制是0001 0010,八进制是22,十六进制是12,十进制是18

%d%x%oprintf 转换说明符允许我们打印标量 值分别为十进制、十六进制和 otcal。 %u 用于打印 无符号值。

编辑

请说明 CPU 中的那些 0 和 1 在什么时候被识别为其他任何东西...

也许你首先要了解的是十进制、十六进制、 八进制,二进制只是标量值的表示。我们人类使用这个 表示来掌握数量的概念。我们选择一个基数 代表固定值的数字。在十进制中,我们有 10 位数字,0, 1, 2 ... 9。 每个数字都有一个固定值,当我们将这些数字组合在一起时,我们可以 表示大于 9 的值。例如序列表示的值 123 等于:

3x100 + 2x101 + 1x102

这就是为什么我们称右边的数字为单位列, 十列的中间,百列左侧的数字。

在什么时候将 0 和 1 重写为 ASCII 字符或数字对我们更有意义?

它们对 CPU 没有任何意义,它们只是值、模式 0 和 1。是我们人类(或者更确切地说是创建 ASCII 表的身体)通过说 当字符 变量的值为 48,我们将其视为'0',即值 0 的字符表示。 CPU 只看到序列 0 和 1,我们人类确定它们的含义,我们的算法是什么 确定我们如何处理这些 0 和 1 序列。

您不能将值与其表示混合。表示只对我们人类有意义。

【讨论】:

  • 请说明 CPU 中的 0 和 1 何时被识别为其他任何内容...
  • @Krpcannon 我不明白你的意思。
  • 什么时候0和1改写成ASCII字符或数字对我们更有意义?
  • 我已经更新了您最近 2 个 cmets,希望对您有所帮助。
【解决方案2】:

二进制intlong'0''1' 数字的ASCII 字符串相同。 int 是 32 位 / 4 字节(在典型的 C 实现中),但每位一个字符的字符串是 32 字节。 ISO C 没有定义转换以打印为 base 2 文本这一事实基本上与计算机如何在内部存储整数无关。

在哪个抽象级别 - 二进制表示会变成十进制还是十六进制?

它不会变形printf 必须计算数字的十六进制、十进制或任何基数表示的数字值。并将这些数字值转换为 ASCII 字符,并将它们存储在缓冲区中(或一次将它们发送到操作系统)。

通常的算法算法是重复模/除以基数。来自我对How do I print an integer in Assembly Level Programming without printf from the c library?的回答:

char *itoa_end(unsigned long val, char *p_end) {
  const unsigned base = 10;
  char *p = p_end;
  do {
    *--p = (val % base) + '0';   // for hex, also need to handle the a-f range...
    val /= base;
  } while(val);                  // runs at least once to print '0' for val=0.

  // write(1, p,  p_end-p);
  return p;  // let the caller know where the leading digit is
}

计算数字的字符串表示形式没有“魔法”,只需使用普通代码进行数学运算(编译为普通 CPU 指令)。它与任何其他接受数字和在char[] 数组中存储一些字节。

libc printf 实现将使用类似的代码将字符存储到缓冲区中。例如,glibc 有一个与此完全相同的内部函数,从缓冲区末尾向后存储,从 printf 和其他一些函数调用。取模会产生以 n 为底的表示的最低有效数字,但该数字在打印顺序中位于最后。

具有变量base 的实际实现将特例基数为 10、基数为 8 和基数 16,因为除以编译时常量比任意情况快得多。并且除/模除以 2 的已知幂可以编译为仅移位/与。但这只是一个实现细节。尽管对于 2 次方基数,您可以按打印顺序获取数字,因为它们仅取决于二进制整数中的位范围,而不是所有其他位。

操作系统是否曾经看到二进制文件,或者是否以某种方式比操作系统更低级别的东西覆盖它。

实际上打印字符与转换为字符串表示是分开的,并且(对于printf)通过fwrite(3) 使用的相同机制发生。 在被 stdio 缓冲后,最终write() 系统调用会要求操作系统将一些字节复制到文件描述符/句柄。

大多数操作系统(包括 Windows 和类似 POSIX 的操作系统,如 Linux 或 OS X)仅具有从文件描述符/句柄读取/写入字节的系统调用。 操作系统永远不会看到 4 字节的二进制整数,C 库在用户空间中完成所有转换。

某些 CPU 模拟器(如 MARS 或 SPIM)具有“系统调用”,可将用户键入的字符串读取到寄存器中的二进制整数中,反之亦然。但是普通的操作系统将这留给用户空间库。


以十六进制编程源文件会比以十进制编程(例如在常量或变量初始化期间)对性能(速度或存储)有任何好处吗?

不,到二进制整数的转换发生在编译时,所以如果源是static int foo = 0xa, bar = 10;,目标文件只包含两个 4 字节的二进制整数,每个都有相同的位模式代表相同的值。

【讨论】:

  • 你的第一句话似乎是对我的问题的一种重述,实际上是我困惑的根源。
【解决方案3】:

计算机中的数字绝不是十进制格式。

问题不是它们什么时候变成二进制,而是什么时候变成十进制。

您应该能够获取二进制数 0b1111011 并将其转换为十进制的 123 和十六进制的 0x7B,而不是通过使用计算器上的基本转换按钮,而是通过了解从基本转换的工作原理,例如 3785 秒是 1 小时 3 分钟和5 秒(从 10 到 60)。

C 库看到你想要十进制,它需要位 0b1111011 在那一刻之前没有任何意义,它们只是位,在此之后它们将回到没有意义的计算机,至少对它们只有意义的计算机你。要得到 100 的位置,你必须除以 0b1100100,结果是 0b1,所以从 0b1111011 中减去 0b1100100,得到 0b10111,现在将其除以 0b1010,得到 0b10,所以从 0b10111 中减去 0b10100(0b1010 乘以 0b10),得到 0b11到目前为止,以 10 为底的转换是 0b1、0b10、0b11。现在 printf 需要从中生成 ASCII,因此它将 0b110000 添加到那些给出 0b110001、0b110010、0b110011 的数字上。并将该“字符串”输入字符输出例程(您会看到 123)。我们从来没有任何小数,它只是被操纵的位。

当你写一些代码时

unsigned int x = 5;

编译器将该 5(在源代码文件中实际上是 0b110101)转换为 0b101,并将其放置在决定存储变量 x 的任何位置。

现在让我们回到 0b1111011 并将其转换为十六进制,从右边开始,一次取 4 位,得到 0b111 和 0b1011,比迄今为止的基数 10 转换要快得多(通常,对于 8位号,如果这是它的话)。您可以添加 0b110000 然后与 0b111001 进行比较的两种方法之一,或者您可以与 0b1001 进行比较然后添加不同的数字。例如,0b111 变为 0b110111,然后检查它是否大于 0b111001,不,所以继续到 0b1011 添加 0b110000,你会得到 0b111011 是否大于 0b111001?是的,所以添加 0b111 或 0b100111 取决于您是想看到大写字母还是小写字母,现在字符串是例如 0b110111,0b1000010 加上终止零,您将其发送到打印并且您会在输出中看到 7B。

十六进制输出会更快是的。收益的多少和相关程度取决于许多因素...

现在我不知道你用十六进制编程源文件是什么意思

unsigned int x = 0x5;

编译时间会比

稍长
unsigned int x = 5;

因为多余的字符。但是

unsigned int x = 0x7B;

unsigned int x = 123;

嗯,十进制可能更快。

unsigned int x = 0x11111111;

unsigned int x = 286331153;

现在您不得不怀疑,在特定机器上,十六进制会更快,数据模式也很重要,如此处所示。

如此处所示,十六进制版本需要多两个字节的存储空间来保存源文件。

unsigned int x = 0x5;

unsigned int x = 5;

编译后的输出与应用于 x (0b101) 的常量相同。因此机器代码(和/或 .data 存储)不仅大小相同,而且相同。

unsigned int fun0 ( void )
{
    return(5);
}
unsigned int fun1 ( void )
{
    return(0x5);
}
unsigned int fun2 ( void )
{
    return(123);
}
unsigned int fun3 ( void )
{
    return(0x7B);
}

给出这个机器码

00000000 <fun0>:
   0:   e3a00005
   4:   e12fff1e

00000008 <fun1>:
   8:   e3a00005
   c:   e12fff1e

00000010 <fun2>:
  10:   e3a0007b
  14:   e12fff1e

00000018 <fun3>:
  18:   e3a0007b
  1c:   e12fff1e

已经存在并且现在存在带有 %b 的 C 库,但它是非标准的,从来没有意义为什么它不是。同样是八进制,嗯,八进制也有一个。

注意八进制转换与十六进制竞争,你没有条件

0b1111011,一次屏蔽和移出 3 位 0b001、0b111、0b011 添加 0x110000,就像您分别给出 0b110001、0b110111、0b110011 一样。所以你没有条件,但你有更多的“字符”要处理,对于 8 位数字,十六进制可以赢,但更大的八进制应该赢。

关于那个话题:

unsigned int fun0 ( void )
{
    return(5);
}
unsigned int fun1 ( void )
{
    return(0x5);
}
unsigned int fun2 ( void )
{
    return(05);
}
unsigned int fun3 ( void )
{
    return(123);
}
unsigned int fun4 ( void )
{
    return(0x7B);
}
unsigned int fun5 ( void )
{
    return(0173);
}

给予

00000000 <fun0>:
   0:   e3a00005
   4:   e12fff1e

00000008 <fun1>:
   8:   e3a00005
   c:   e12fff1e

00000010 <fun2>:
  10:   e3a00005
  14:   e12fff1e

00000018 <fun3>:
  18:   e3a0007b
  1c:   e12fff1e

00000020 <fun4>:
  20:   e3a0007b
  24:   e12fff1e

00000028 <fun5>:
  28:   e3a0007b
  2c:   e12fff1e

所以就源代码的“存储”而言,5 比 05 便宜,比 0x5 便宜,但 0x7B 与 0173 相同,但 123 更便宜。随着数字变大,十六进制变得最便宜(显然它具有更高的基数 16 vs 8 vs 10)。

您真的那么渴望源代码的存储空间吗?那么你需要成为一个标签人而不是一个太空人。并使用短变量名和函数名。我的长答案可能已经填满了你所有的内存。

【讨论】:

  • 位是位,“计算机”不知道来自 ASCII 常量的地址,它们只是位。对于指令的持续时间,一些位例如是地址。但有时这些相同的位不是地址而是操作数:ptr++;这些位对人类来说意味着什么,计算机可能不在乎。 ASCII 对计算机绝对没有任何意义,最接近它的相关性可能是偏移到一个位表中,人类会将其解释为像素,但计算机只是位。
  • 这就像问砖头是否知道它是图书馆的一部分,或者另一个砖是否知道它是房子的一部分。在宏伟的计划中,它只是一块砖。对人类来说,它们的集合就成了房子,但它们实际上只是单独的砖块。
  • 指令与数据是同一个问题,这往往会让那些明白其他一切都只是比特的人大吃一惊。 cpu如何从数据中分辨指令?它没有,它处理输入给它的位。由人类来提供正确的位,数据进入处理器,它进入管道,有很多常量和地址与二进制指令混合在一起,这些进入管道但人类已经放置了它们这样他们就不会被执行(希望如此)。这些位仅与少量时钟周期的指令相关。
  • 对于加法和减法,cpu 不知道有符号和无符号,这只与人类有关。乘法是有符号与无符号很重要,但编译器(存在变量类型(在语言中))选择正确的乘法或除法(有时有符号与无符号与乘法或除法无关)。将把这个(与其他练习一样)留给读者自己弄清楚。
猜你喜欢
  • 2014-10-30
  • 1970-01-01
  • 2011-12-09
  • 2018-07-26
  • 2014-04-20
  • 1970-01-01
  • 2014-01-07
  • 2012-04-08
相关资源
最近更新 更多