新的C语言:一切都源于FORTRAN

2024-04-07 17:32
文章标签 语言 源于 fortran

本文主要是介绍新的C语言:一切都源于FORTRAN,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

新的C语言:一切都源于FORTRAN

此篇文章摘取于即将登载于《Dr.Dobb's 软件研发》第三期(2003年10月)的《The New C:一切源于FORTRAN》,文章主要是介绍了C99的新特性受限指针,在得到作者Randy Meyers以及《Dr.Dobb's 软件研发》杂志负责人刘江先生的应允下,把全文的前面的一部分作为文档发表,希望能对大家有所帮助。

 

 

新的C语言:一切都源于FORTRAN

译注:本文是作者Randy Meyers在 CUJ杂志开的一个专题系列The New C的第二篇文章,主要是叙说C99中的新关键字restrict以及受限指针(restricted pointers)的历史渊源和使用方式。受限指针作为一种编译器优化代码的方式,是由编译器厂商提供特定的实现,因此这篇文章所谈论的并非在一切实现中都能得到支持,至于如何使用restrict关键字,这篇文章做了很好的说明,期望本文能给关心C语言和使用C语言的用户带来帮助。在翻译上,所有译者在翻译过程中有疑惑的术语或者其他一切都以括号形式把原文直接给出,诚心不想给读者半点误导,但是否如愿还需读者的评判,关于本文的一切可以用amstrongest@hotmail.com与译者联系和讨论。

 

有时候改进一种语言的最好方式就是让它和三十年前的古老样子更相似

 

 

 

一切都源于FORTRANIt all began with FORTRAN)。

 

谈起上面的话,我并不是想说FORTRAN是第一个程序设计语言,但是在上个世纪六十年代(1960s)的一场关于如何在FORTRAN中实现参数传递的争论,却意外的使FORTRAN在七十年代(1970s)的超级计算机上面的性能有了巨大的提升,并且导致了九十年代(1990s)一个关于C语言的新特征被C99所接受,这就是受限指针(restricted pointers)。而理解受限指针的原始动机的最好方式就是回顾历史,重温发生在FORTRAN中的那个由争论所导致的意外。

 

C不一样的是,在FORTRAN中如果一个函数被分配了一个新值作为参数,传递给函数的实参值将会改变,并且在函数返回时,调用者将会得到新的参数值。考虑下面Example 1所例举的代码,如果你以Y作为参数调用F,在F返回时,Y值将会是6[译注:下面的程序没有出现变量Y,文中意思是Y是实参数,而下面程序出现的X是形参数,只是属于函数F的内部变量,但是当把Y复制给X后,并且改变X同时将改变外面调用的Y的值]

Example 1:

SUBROUTINE F(X)
INTEGER X
X = 6
END

这样的参数传递方式就使争论随之而来。不同的FORTRAN编译器可以选择两种实现方式中的一种来获得FORTRAN中的参数传递语义。第一种方式是引用参数传递(by reference),也就是典型的C程序员所使用的:写一个函数,并且在它的调用者中修改变量。(write a function that modifies a variable in its caller)。传递给函数的是一个参数的地址,并且在需要的时候任何地方都可以间接访问这个参数。如果FORTRAN编译器产生C代码的话,就会和下面Example 2.C代码类似。

Example 2:

void f(int *x)
   {
   *x = 6;
   }

然而,对于一些类型的计算机来说,间接访问局部变量比直接引用访问所带来的运行时开销要大的多。这也就导致了FORTRAN中参数传递的第二种实现方式。实参的地址依然会被传递给函数,但是函数一旦被调用,就将生成一个实参数的局部拷贝[译注:传递的是地址,但是函数内部拷贝的却是参数值],在函数生存期中将一直使用这个拷贝的局部变量,当函数返回时,将把拷贝变量赋值给调用函数的参数变量。这样的FORTRAN编译器如果产生C代码将会和下面的Example 3相类似。进/出拷贝(copy in/copy out)参数传递方式增加了函数进入和返回时的负担,但是如果一个参数被多次引用,而间接引用(在一些机器上代价十分昂贵)却不再使用的话,导致的结果就是性能的提升(在一些机器上面而言)。[译注:就是说第一种方式的主要的调用开销是间接引用,第二种方式的主要调用开销是拷贝变量,其中哪种更好,需要根据真实代码的情况衡量决定]

Example 3:

void f(int *x)
   {
   int xcopy = *x;
   xcopy = 6;
   *x = xcopy;
   }

大多数时候,编译器如何实现语言特征通常都被认为不过只是“实现细节”。它们不会影响程序员编写程序的方式,而语言的标准委员会允许语言的实现者自由选择和改变实现方式。然而,根据使用的参数传递机制,FORTRAN程序会产生不同的结果。考虑下面Example 4中的FORTRAN代码,以及以两种方式转换成的C代码:

Example 4:

SUBROUTINE G(X, Y)
INTEGER X, Y
X = 1 + X
Y = 5 + Y
END
 
// Translation using "by reference"
void g(int *x, int *y)
{
   *x = 1 + *x;
   *y = 5 + *y;
}
 
// Translation using
// "copy in/copy out"
void g(int *x, int *y)
{
   int xcopy = *x;
   int ycopy = *y;
   xcopy = 1 + xcopy;
   ycopy = 5 + ycopy;
   *x = xcopy;
   *y = ycopy;
}

G函数给它的参数加上了不同的常量,如果你把参数AB传递给函数G,并且在调用前A的值是1B的值是10。不用怀疑,无论使用FORTRAN中的那种函数参数传递机制,当函数返回时A的值将变成2B的值将变成15。但是请考虑,如果你传递参数都是的A(并且被初始化为1),将会是什么情况?如果是使用引用调用(by reference)的参数传递机制,在函数返回时A将的值将变成7A的值在赋值给*x的过程中时候被更新,因为xy都指向A,所以在随后的对*y赋值的过程中A的值将再次被改写。相反,如果是使用进/出拷贝(copy in/copy out)的参数传递机制,在函数返回时,A的值将是6。调用发生后,在函数G中将不同的拷贝变量,并且每一个都将在函数返回时赋值给A,但最后的一个拷贝变量的返回值才会成为A的终值。

 

这两种不同的参数传递机制是任何程序设计语言定义者所必须面对的不一致性的代表。语言需要特殊的实现实现技术吗?也许这将会以付出效率为代价?语言的特性是否应该为了避免争议而改变?FORTRAN的定义者因为效率而允许同时存在两种参数传递机制。而一旦这样的决定做了出来,某种类型的程序就变的不一致了,并将导致无法定义的结果(outlawed)。

 

FORTRAN 66 标准包含了一系列可能会误导程序员的规则。在函数参数列表中,对于任何变量你都只能传递一次。如果你传递了一个变量作为函数参数,那么这个函数就不能再在全局上引用这个变量(FORTRAN COMMON)。如果你传递给一个变量给函数,你就不能再传递任何东西,并且这个函数也不能再引用任何东西,that overlaps it in storage (FORTRAN EQUIVALENCE)。在这样的规则下,没有什么程序可以确定应该采用何种参数传递机制。

 

大约十年以后[译注:意指1970s],为了实现超级计算机Cray 1的高性能,超级计算机需要高优化的编译器来使传统的程序能够使用机器的向量寄存器(vector registers)。考虑Example 5中的程序。其中对于函数来说最有效率的代码就是先后把数组指针xy载入到向量寄存器中然后执行向量加指令来把两个向量寄存器中的变量加在一起。如果编译器以产生向量指令的方式来取代传统的使用循环来访问数组中的每一个元素的方式,那么代码的运行效率将得到巨大的提升。

Example 5:

void
vector_add(float *x, float *y,
   float *result)
   {
   int i;
   for (i = 0; i < 64; ++i)
      result[i] = x[i] + y[i];
   }

编译器中的优化器肯定会把循环转化成一系列的向量指令,但是问题在于那些向量指令是否真的whether the sequence of vector instructions is really equivalent to the original loop。你能在处理result数组的存储工作之前就把x,y数组载入到向量寄存器中,只因为你清楚result数组和x,y数组都是不同的个体。考虑如果result指向x[1],将会发生什么?在这种情况下result[0]其实就是x[1],同样result[I]其实就是x[I+1],每一次循环迭代过程中都会存储下一次的迭代中会被引用的变量。如果在做result的存储工作之前就把x载入到向量寄存器中去,变量值将会改变calculated change。正是在这一点上,FORTRAN的定义就带来了冲突。为了避免在传递机制中需要引入一个特殊的参数,FORTRAN标准定义了一系列精确的规则用来允许向量化编译器(vectorizing compiler)假设x,yresult都是互不相关的,non-overlapping arrays。就这样偶然的,FORTRAN在向量机上就有了巨大的性能优势。

这篇关于新的C语言:一切都源于FORTRAN的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/883226

相关文章

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要

C语言中自动与强制转换全解析

《C语言中自动与强制转换全解析》在编写C程序时,类型转换是确保数据正确性和一致性的关键环节,无论是隐式转换还是显式转换,都各有特点和应用场景,本文将详细探讨C语言中的类型转换机制,帮助您更好地理解并在... 目录类型转换的重要性自动类型转换(隐式转换)强制类型转换(显式转换)常见错误与注意事项总结与建议类型

Go语言利用泛型封装常见的Map操作

《Go语言利用泛型封装常见的Map操作》Go语言在1.18版本中引入了泛型,这是Go语言发展的一个重要里程碑,它极大地增强了语言的表达能力和灵活性,本文将通过泛型实现封装常见的Map操作,感... 目录什么是泛型泛型解决了什么问题Go泛型基于泛型的常见Map操作代码合集总结什么是泛型泛型是一种编程范式,允

Android kotlin语言实现删除文件的解决方案

《Androidkotlin语言实现删除文件的解决方案》:本文主要介绍Androidkotlin语言实现删除文件的解决方案,在项目开发过程中,尤其是需要跨平台协作的项目,那么删除用户指定的文件的... 目录一、前言二、适用环境三、模板内容1.权限申请2.Activity中的模板一、前言在项目开发过程中,尤

C语言小项目实战之通讯录功能

《C语言小项目实战之通讯录功能》:本文主要介绍如何设计和实现一个简单的通讯录管理系统,包括联系人信息的存储、增加、删除、查找、修改和排序等功能,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录功能介绍:添加联系人模块显示联系人模块删除联系人模块查找联系人模块修改联系人模块排序联系人模块源代码如下

基于Go语言实现一个压测工具

《基于Go语言实现一个压测工具》这篇文章主要为大家详细介绍了基于Go语言实现一个简单的压测工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录整体架构通用数据处理模块Http请求响应数据处理Curl参数解析处理客户端模块Http客户端处理Grpc客户端处理Websocket客户端

使用SQL语言查询多个Excel表格的操作方法

《使用SQL语言查询多个Excel表格的操作方法》本文介绍了如何使用SQL语言查询多个Excel表格,通过将所有Excel表格放入一个.xlsx文件中,并使用pandas和pandasql库进行读取和... 目录如何用SQL语言查询多个Excel表格如何使用sql查询excel内容1. 简介2. 实现思路3

Go语言实现将中文转化为拼音功能

《Go语言实现将中文转化为拼音功能》这篇文章主要为大家详细介绍了Go语言中如何实现将中文转化为拼音功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 有这么一个需求:新用户入职 创建一系列账号比较麻烦,打算通过接口传入姓名进行初始化。想把姓名转化成拼音。因为有些账号即需要中文也需要英

Go语言使用Buffer实现高性能处理字节和字符

《Go语言使用Buffer实现高性能处理字节和字符》在Go中,bytes.Buffer是一个非常高效的类型,用于处理字节数据的读写操作,本文将详细介绍一下如何使用Buffer实现高性能处理字节和... 目录1. bytes.Buffer 的基本用法1.1. 创建和初始化 Buffer1.2. 使用 Writ