C数据类型如何“被大多数计算机直接支持”?


114

我正在阅读K&R的“ C编程语言”,并发现了这一说法[Introduction,p。1。3]:

因为大多数计算机直接支持C提供的数据类型和控制结构,所以实现自包含程序所需的运行时库很小。

粗体字表示什么意思?是否有计算机直接支持的数据类型或控制结构的示例?


1
如今,C语言确实支持复杂的算术运算,但是最初它不是因为计算机不直接支持将复数作为数据类型而支持。
乔纳森·莱夫勒

12
实际上,从历史的角度来看,恰恰相反:C是从当时可用的硬件操作和类型设计的。
Basile Starynkevitch

2
大多数计算机不直接支持十进制浮点数的硬件
PlasmaHH 2015年

3
@MSalters:我试图向某个方向暗示“是否存在计算机不直接支持的数据类型或控件结构的示例?”的问题。我并没有解释为仅限于K&R
PlasmaHH 2015年

11
Stack Overflow启动超过6年后,这又不是重复吗?
彼得·莫滕森

Answers:


143

是的,有些数据类型不直接支持。

在许多嵌入式系统上,没有硬件浮点单元。因此,当您编写如下代码时:

float x = 1.0f, y = 2.0f;
return x + y;

它被翻译成这样的东西:

unsigned x = 0x3f800000, y = 0x40000000;
return _float_add(x, y);

然后,编译器或标准库必须提供的实现_float_add(),该实现占用嵌入式系统上的内存。如果要在一个很小的系统上计算字节数,这可能会加起来。

另一个常见的例子是64位整数(long long自1999年以来为C标准),而32位系统不直接支持它们。旧的SPARC系统不支持整数乘法,因此必须由运行时提供乘法。还有其他例子。

其他语言

相比之下,其他语言具有更复杂的原语。

例如,Lisp符号需要大量的运行时支持,就像Lua中的表,Python中的字符串,Fortran中的数组等等。C中的等效类型通常根本不是标准库的一部分(没有标准符号或表),或者它们更简单并且不需要太多的运行时支持(C中的数组基本上只是指针,以n结尾的字符串是几乎一样简单)。

控制结构

C语言中缺少的一个值得注意的控制结构是异常处理。非本地出口仅限于setjmp()longjmp(),它们仅保存和恢复处理器状态的某些部分。相比之下,C ++运行时必须遍历堆栈并调用析构函数和异常处理程序。


2
基本上只是指针...而是基本上只是原始的内存块。即使那是挑剔的,但答案还是不错的。
Deduplicator

2
您可能会争辩说,以空终止的字符串具有“硬件支持”,因为字符串终止符适合大多数处理器的“如果为零,则跳转”操作,因此比其他可能的字符串实现要快一些。
彼得斯(Peteris)

1
发表了我自己的答案,以扩展C如何设计为简单地映射到asm。
彼得·科德斯

1
请不要使用“数组基本上只是指针”的搭配,它会严重,严重地误导OP之类的初学者。更好的做法是“在硬件级别使用指针直接实现数组”。
顺磁牛角包

1
@TheParamagneticCroissant:在这种情况下,我认为这是适当的...清晰度是以准确性为代价的。
Dietrich Epp 2015年

37

实际上,我敢打赌,自1978年Kernighan和Ritchie在书的第一版中首次编写这些内容以来,引言的内容并没有太大变化,并且它们在那时比在更现代的意义上参考了C的历史和演变。实现。

从根本上说,计算机只是存储库和中央处理器,每个处理器都使用机器代码运行。每个处理器设计的一部分是指令集架构,称为汇编语言,该将一组人类可读的助记符一对一地映射到机器码(全数字)。

C语言的作者以及紧随其后的B和BCPL语言的作者都希望使用尽可能有效地编译为汇编语言的结构来定义……实际上,由于目标的局限性,他们不得不这样做硬件。正如其他答案所指出的那样,这涉及分支(GOTO和C中的其他流控制),移动(分配),逻辑运算(&| ^),基本算术(加,减,递增,递减)和内存寻址(指针)。 )。一个很好的例子是C中的pre / post递增和递减运算符,据说它们是Ken Thompson添加到B语言中的,特别是因为它们能够在编译后直接转换为单个操作码。

这就是作者说“大多数计算机直接支持”的意思。它们并不意味着其他语言包含直接支持的类型和结构,而是意味着通过设计 C结构可以直接翻译(有时字面意思是直接)转换为Assembly。

与底层程序集的这种紧密联系,尽管仍然提供了结构化编程所需的所有元素,却导致了C语言的早期采用,并使它在当今仍然是关键的编译效率环境中成为流行语言。

有关该语言历史的有趣文章,请参见《 C语言的发展-Dennis Ritchie》。


14

简短的答案是,目标计算机的微处理器也支持C支持的大多数语言构造,因此,编译后的C代码非常好且高效地转换为微处理器的汇编语言,从而导致代码更小,占用空间更小。

较长的答案需要一点汇编语言知识。在C中,这样的语句:

int myInt = 10;

会在汇编中转换为如下形式:

myInt dw 1
mov myInt,10

将此与类似C ++的东西进行比较:

MyClass myClass;
myClass.set_myInt(10);

产生的汇编语言代码(取决于MyClass()的大小)可能会增加多达数百条汇编语言行。

如果没有实际使用汇编语言创建程序,纯C可能是您可以在其中编写程序的“最细”和“最紧”的代码。

编辑

鉴于对我的回答的评论,我决定进行测试,只是出于我自己的理智。我创建了一个名为“ test.c”的程序,如下所示:

#include <stdio.h>

void main()
{
    int myInt=10;

    printf("%d\n", myInt);
}

我使用gcc将其编译为汇编。我使用以下命令行对其进行了编译:

gcc -S -O2 test.c

这是生成的汇编语言:

    .file   "test.c"
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "%d\n"
    .section    .text.unlikely,"ax",@progbits
.LCOLDB1:
    .section    .text.startup,"ax",@progbits
.LHOTB1:
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB24:
    .cfi_startproc
    movl    $10, %edx
    movl    $.LC0, %esi
    movl    $1, %edi
    xorl    %eax, %eax
    jmp __printf_chk
    .cfi_endproc
.LFE24:
    .size   main, .-main
    .section    .text.unlikely
.LCOLDE1:
    .section    .text.startup
.LHOTE1:
    .ident  "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1"
    .section    .note.GNU-stack,"",@progbits

然后,我创建一个名为“ test.cpp”的文件,该文件定义了一个类,并输出了与“ test.c”相同的内容:

#include <iostream>
using namespace std;

class MyClass {
    int myVar;
public:
    void set_myVar(int);
    int get_myVar(void);
};

void MyClass::set_myVar(int val)
{
    myVar = val;
}

int MyClass::get_myVar(void)
{
    return myVar;
}

int main()
{
    MyClass myClass;
    myClass.set_myVar(10);

    cout << myClass.get_myVar() << endl;

    return 0;
}

我使用以下命令以相同的方式进行编译:

g++ -O2 -S test.cpp

这是生成的程序集文件:

    .file   "test.cpp"
    .section    .text.unlikely,"ax",@progbits
    .align 2
.LCOLDB0:
    .text
.LHOTB0:
    .align 2
    .p2align 4,,15
    .globl  _ZN7MyClass9set_myVarEi
    .type   _ZN7MyClass9set_myVarEi, @function
_ZN7MyClass9set_myVarEi:
.LFB1047:
    .cfi_startproc
    movl    %esi, (%rdi)
    ret
    .cfi_endproc
.LFE1047:
    .size   _ZN7MyClass9set_myVarEi, .-_ZN7MyClass9set_myVarEi
    .section    .text.unlikely
.LCOLDE0:
    .text
.LHOTE0:
    .section    .text.unlikely
    .align 2
.LCOLDB1:
    .text
.LHOTB1:
    .align 2
    .p2align 4,,15
    .globl  _ZN7MyClass9get_myVarEv
    .type   _ZN7MyClass9get_myVarEv, @function
_ZN7MyClass9get_myVarEv:
.LFB1048:
    .cfi_startproc
    movl    (%rdi), %eax
    ret
    .cfi_endproc
.LFE1048:
    .size   _ZN7MyClass9get_myVarEv, .-_ZN7MyClass9get_myVarEv
    .section    .text.unlikely
.LCOLDE1:
    .text
.LHOTE1:
    .section    .text.unlikely
.LCOLDB2:
    .section    .text.startup,"ax",@progbits
.LHOTB2:
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB1049:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $10, %esi
    movl    $_ZSt4cout, %edi
    call    _ZNSolsEi
    movq    %rax, %rdi
    call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
    xorl    %eax, %eax
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LFE1049:
    .size   main, .-main
    .section    .text.unlikely
.LCOLDE2:
    .section    .text.startup
.LHOTE2:
    .section    .text.unlikely
.LCOLDB3:
    .section    .text.startup
.LHOTB3:
    .p2align 4,,15
    .type   _GLOBAL__sub_I__ZN7MyClass9set_myVarEi, @function
_GLOBAL__sub_I__ZN7MyClass9set_myVarEi:
.LFB1056:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $_ZStL8__ioinit, %edi
    call    _ZNSt8ios_base4InitC1Ev
    movl    $__dso_handle, %edx
    movl    $_ZStL8__ioinit, %esi
    movl    $_ZNSt8ios_base4InitD1Ev, %edi
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    jmp __cxa_atexit
    .cfi_endproc
.LFE1056:
    .size   _GLOBAL__sub_I__ZN7MyClass9set_myVarEi, .-_GLOBAL__sub_I__ZN7MyClass9set_myVarEi
    .section    .text.unlikely
.LCOLDE3:
    .section    .text.startup
.LHOTE3:
    .section    .init_array,"aw"
    .align 8
    .quad   _GLOBAL__sub_I__ZN7MyClass9set_myVarEi
    .local  _ZStL8__ioinit
    .comm   _ZStL8__ioinit,1,1
    .hidden __dso_handle
    .ident  "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1"
    .section    .note.GNU-stack,"",@progbits

正如您可以清楚地看到的那样,生成的汇编文件在C ++文件上要比在C文件上大得多。即使您删除了所有其他内容,然后仅将C“ main”与C ++“ main”进行比较,也有很多额外的内容。


14
“ C ++代码”不是C ++。而且像MyClass myClass { 10 }C ++ 这样的真实代码很可能会编译为完全相同的程序集。现代C ++编译器消除了抽象损失。结果,它们经常可以击败C编译器。例如,C中的抽象代价qsort是真实的,但是std::sort即使经过基本优化,C ++ 也没有抽象代价。
MSalters 2015年

1
您可以很容易地看到使用IDA Pro,大多数C ++构造都可以像在C中手动编译一样进行编译,构造函数和dtor内联了琐碎的对象,然后应用了未来的优化
paulm,2015年

7

K&R表示大多数C表达式(技术含义)映射到一个或几个汇编指令,而不是对支持库的函数调用。通常的例外情况是在没有硬件div指令的体系结构上进行整数除法,或者在没有FPU的计算机上进行浮点运算。

有一个报价:

C将汇编语言的灵活性和强大功能与汇编语言的用户友好性相结合。

在这里找到。我想我还记得一个不同的变化,例如“汇编语言的速度与汇编语言的便捷性和表达性”。)

long int通常与本机寄存器的宽度相同。

一些高级语言定义了其数据类型的确切宽度,并且所有计算机上的实现都必须工作相同。但是不是C。

如果要在x86-64上使用128位整数,或者在一般情况下使用任意大小的BigInteger,则需要一个函数库。现在,所有CPU都使用2s补码作为负整数的二进制表示形式,但是即使在设计C时也是如此。(这就是为什么在非2s补码机上会产生不同结果的某些事情在C标准中在技术上是未定义的。)

指向数据或函数的C指针的工作方式与程序集地址相同。

如果您需要引用计数引用,则必须自己进行。如果要让c ++虚拟成员函数根据指针所指向的对象类型调用不同的函数,则C ++编译器必须生成的不仅仅是一个call一个固定地址指令。

字符串只是数组

在库函数之外,仅提供的字符串操作是读/写字符。没有concat,没有子字符串,没有搜索。(字符串'\0'以8位整数的nul终止()数组存储,而不是指针+长度,因此,要获得子字符串,必须将nul写入原始字符串。)

CPU有时具有专为字符串搜索功能使用的指令,但通常仍会在循环中每执行一条指令处理一个字节。(或使用x86 rep前缀。也许如果C是在x86上设计的,则字符串搜索或比较将是本机操作,而不是库函数调用。)

许多其他答案还提供了本机不支持的示例,例如异常处理,哈希表,列表。K&R的设计理念是C本身没有这些的原因。


“ K&R意味着大多数C表达式(技术含义)映射到一个或几个汇编指令,而不是对支持库的函数调用。” 这是一个非常直观的解释。谢谢。
gwg 2015年

1
我刚刚遇到过“冯·诺依曼语言”一词(en.wikipedia.org/wiki/Von_Neumann_programming_languages)。正是C是什么。
彼得·科德斯

1
这正是我使用C的原因。但是,当我学习C时,令我感到惊讶的是,通过尝试对广泛的硬件都有效,它在大多数现代硬件上有时是无效的且效率低下的。我的意思是,例如,检测C中整数溢出的无用且可靠的方法以及使用进位标志的多字添加
Z玻色子2015年

6

进程的汇编语言通常处理跳转(转到),语句,移动语句,二进制变量(XOR,NAND,AND OR等),内存字段(或地址)。将内存分为两种类型:指令和数据。这就是汇编语言的全部内容(我相信汇编程序员会争论的不仅仅是它,但是总的来说,归结为这一点)。C非常类似于这种简单性。

C是组装代数对算术。

C封装了汇编的基本知识(处理器的语言)。可能比“因为大多数计算机直接支持C提供的数据类型和控制结构”更真实。


5

提防误导性比较

  1. 该声明依赖于“运行时库”概念,此后至少在主流高级语言中已经过时了。(它仍然与最小的嵌入式系统有关。)运行时是仅使用语言内置的结构时(相对于显式调用库提供的函数的情况),该语言程序所需要执行的最小支持。 。
  2. 相反,现代语言倾向于不区分运行时库和标准库,后者通常是相当广泛的。
  3. 在撰写K&R书时,C甚至没有标准。相反,可用的C库在Unix的不同版本之间有很大的不同。
  4. 为了理解该语句,您不应将其与具有标准库的语言(如其他答案中提到的Lua和Python)进行比较,而应与具有更多内置结构的语言(如在其他文章中提到的老式LISP和老式FORTRAN)进行比较。答案)。其他示例是BASIC(交互式,例如LISP)或PASCAL(编译,例如FORTRAN),它们都具有(除其他外)在语言本身中内置的输入/输出功能。
  5. 相比之下,没有标准方法可以从仅使用运行时而不使用任何库的C程序中获取计算结果。

另一方面,大多数现代语言都在专用运行时环境中运行,这些运行时环境提供了诸如垃圾收集的功能。
Nate CK

5

有没有计算机不直接支持的数据类型或控制结构的示例?

所有基本数据类型及其在C语言中的操作都可以通过一个或几个机器语言指令来实现,而无需循环-它们(实际上是每个CPU)直接支持它们。

几种流行的数据类型及其操作需要数十条机器语言指令,或者需要迭代某些运行时循环,或者两者都需要。

许多语言对于此类类型及其操作都有特殊的缩写语法-在C语言中使用此类数据类型通常需要键入更多代码。

这些数据类型和操作包括:

  • 任意长度的文本字符串操作-连接,子字符串,将新字符串分配给以其他字符串初始化的变量等,等等('s =“ Hello World!”; s =(s + s)[2:-2] '(在Python中)
  • 具有嵌套虚拟析构函数的对象,例如C ++和其他所有面向对象的编程语言
  • 2D矩阵乘法和除法;解决线性系统(MATLAB和许多数组编程语言中的“ C = B / A; x = A \ b”)
  • 常用表达
  • 可变长度数组-特别是在数组末尾附加一个项目,这有时需要分配更多的内存。
  • 在运行时读取更改类型的变量的值-有时是浮点数,其他时候是字符串
  • 关联数组(通常称为“地图”或“字典”)
  • 清单
  • 比率(“(+ 1/3 2/7)” 在Lisp中给出“ 13/21” )
  • 任意精度算术(通常称为“ bignums”)
  • 将数据转换为可打印的表示形式(JavaScript中的“ .tostring”方法)
  • 饱和定点数(通常在嵌入式C程序中使用)
  • 计算在运行时键入的字符串,就好像它是一个表达式(许多编程语言中为“ eval()”)一样。

所有这些操作都需要数十条机器语言指令,或者需要在几乎每个处理器上迭代一些运行时循环。

一些流行的控制结构也需要数十种机器语言指令或循环,包括:

  • 关闭
  • 延续
  • 例外情况
  • 懒惰的评价

无论用C语言还是其他某种语言编写,当程序处理此类数据类型时,CPU最终都必须执行处理这些数据类型所需的任何指令。这些说明通常包含在“库”中。每种编程语言,甚至C语言,都有针对每个平台的“运行时库”,默认情况下包含在每个可执行文件中。

大多数编写编译器的人都将用于处理“内置于语言中”的所有数据类型的指令放入其运行时库中。由于C 语言没有在语言中内置上述任何数据类型,操作和控制结构,因此C运行时库中都不包含它们-这使得C运行时库小于运行时库其他编程语言的时间库,其中包含上述语言的更多内容。

当程序员希望使用C或他选择的任何其他语言的程序来处理非“内置于该语言中”的其他数据类型时,该程序员通常会告诉编译器在该程序中包括其他库,有时(以“避免依赖关系”)直接在程序中编写这些操作的另一个实现。


如果您的Lisp实施评估为(+ 1/3 2/7)为3/21,我认为您必须具有特别有创意的实施...
RobertB 2015年

4

什么是内置数据类型 C?他们之类的东西intchar* intfloat,数组等等......这些数据类型由CPU的理解。CPU知道如何使用数组,如何取消对指针的引用以及如何对指针,整数和浮点数执行算术运算。

但是,当您使用高级编程语言时,您已经内置了抽象数据类型和更复杂的结构。例如,查看C ++编程语言中大量的内置类。CPU不理解类,对象或抽象数据类型,因此C ++运行时弥合了CPU和语言之间的鸿沟。这些是大多数计算机不直接支持的数据类型的示例。


2
x86知道可以使用某些数组,但不是全部。对于大或不寻常的元素大小,将需要执行整数运算以将数组索引转换为指针偏移量。在其他平台上,这始终是必需的。CPU不理解C ++类的想法是可笑的。只是指针偏移,就像C结构一样。您不需要为此的运行时。
MSalters 2015年

@MSalters是的,但是像iostreams这样的标准库类的实际方法是库函数,而不是由编译器直接支持的。但是,他们可能将其与之比较的高级语言不是C ++,而是当代语言,例如FORTRAN和PL / I。
Random832

1
具有虚拟成员函数的C ++类不仅可以将偏移量转换为结构,还可以转换为更多内容。
彼得·科德斯

4

这取决于计算机。在发明了C的PDP-11上,long支持不佳(您可以购买一个可选的附加模块来支持某些(但不是全部)32位操作)。在任何16位系统上,包括原始IBM PC,在不同程度上都是如此。同样,对于32位计算机或32位程序中的64位操作,尽管在编写K&R书时的C语言根本没有任何64位操作。当然,整个80年代和90年代有很多系统[包括386和486处理器],甚至今天的某些嵌入式系统都没有直接支持浮点算术(floatdouble)。

举一个更奇特的例子,某些计算机体系结构仅支持“面向字的”指针(指向内存中的两个字节或四个字节的整数),并且字节指针(char *void *)必须通过添加额外的偏移量字段来实现。这个问题会详细介绍此类系统。

它所指的“运行时库”功能不是您将在手册中看到的功能,而是现代编译器的运行时库中的此类功能,这些功能用于实现机器支持的基本类型操作。K&R自己指的运行时库可以在Unix Heritage Society的网站上找到 -您可以看到类似ldiv的函数(与同名的C函数不同,当时还不存在),这些函数用于实现PDP-11不支持的32位值,即使使用该插件也是如此csv(以及cretcsv.c中的值),这些值在堆栈上保存和恢复寄存器以管理函数的调用和返回。

他们可能还会提到他们选择不支持底层机器不直接支持的许多数据类型,这与其他现代语言(例如FORTRAN)不同,后者的数组语义不能很好地映射到CPU的底层指针支持,例如C的数组。C数组始终为零索引并且在所有列中始终具有已知大小,但第一个事实意味着无需存储数组的索引范围或大小,也不需要运行时库函数来访问它们-编译器可以简单地对必要的指针算法进行硬编码。


3

该语句仅表示C中的数据和控制结构是面向机器的。

这里有两个方面需要考虑。一种是C语言有一个定义(ISO标准),它允许自由定义数据类型。这意味着C语言实现是针对计算机量身定制的。C编译器的数据类型与编译器针对的计算机中的可用数据类型匹配,因为该语言对此具有一定的自由度。如果一台机器的字长异常,例如36位,则可以使类型intlong符合该字长。假定int正好是32位的程序将中断。

其次,由于这种便携性问题,还有第二个效果。在某种程度上,K&R中的语句已经变成一种自我实现的预言,或者可能是相反的。也就是说,新处理器的实现者意识到支持C编译器的迫切需求,并且他们知道存在许多C代码,这些代码假定“每个处理器看起来都像80386”。架构设计时要考虑C:不仅要考虑C,而且还要考虑到关于C可移植性的常见误解。您根本不能介绍具有9位字节的机器或任何其他用于通用用途的机器。假定类型的程序char恰好8位宽会中断。只有可移植性专家编写的某些程序才能继续工作:可能还不足以合理的努力将一个完整的系统与一个工具链,内核,用户空间和有用的应用程序组合在一起。换句话说,C类型看起来像可从硬件获得的类型,因为使该硬件看起来像是为其编写了许多不可移植的C程序的某些其他硬件。

有没有计算机不直接支持的数据类型或控制结构的示例?

许多机器语言不直接支持的数据类型:多精度整数;链表;哈希表; 字符串。

大多数机器语言不直接支持的控制结构:一流的延续;协程/线程; 发电机; 异常处理。

所有这些都需要使用大量通用指令和更多基本数据类型创建的可观的运行时支持代码。

C具有某些机器不支持的一些标准数据类型。从C99开始,C具有复数。它们由两个浮点值组成,并可以与库例程一起使用。有些机器根本没有浮点单元。

关于某些数据类型,尚不清楚。如果机器支持使用一个寄存器作为基址,另一个使用缩放位移来寻址内存,这是否意味着数组是直接支持的数据类型?

同样,谈到浮点,存在标准化:IEEE 754浮点。为什么您的C编译器具有double与处理器支持的浮点格式一致的原因,不仅是因为两者是一致的,而且还因为该表示具有独立的标准。


2

诸如

  • 列表几乎在所有功能语言中使用。

  • 例外情况

  • 关联数组(Maps)-包括在PHP和Perl中。

  • 垃圾收集

  • 数据类型/控制结构包含在多种语言中,但不受CPU直接支持。


2

直接支持应该被理解为有效地映射到处理器的指令集。

  • 除了长(可能需要扩展的算术例程)和短大小(可能需要屏蔽)之外,直接支持整数类型是规则。

  • 对浮点类型的直接支持要求FPU可用。

  • 直接支持位字段是例外。

  • 结构和数组需要地址计算,在某种程度上可以直接支持。

  • 始终总是通过间接寻址直接支持指针。

  • 无条件/条件分支直接支持goto / if / while / for / do。

  • 应用跳转表时可以直接支持该开关。

  • 堆栈功能直接支持函数调用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.