其实本来我的需求挺简单的,我甚至不能说自己是个程序员,各种类库,API什么的我也只是简单了解过boost,ZThread之类常用的,连怎么在windows里用MFC画个窗体都不知道....我主要是个搞算法的人,这几年接触最多的是matlab...其次才是汇编和C++.
介于工作中用到各种数学方面的方法,以及matlab与C++结合时非常恶心的效率和链接方式,以及我对汇编还算略微熟悉,我决定写一套跨平台比较适合我自己的数学库.是,有人说这样的库很多,优秀的也很多,但我还是想自己实现一个,一方面是我这人比较喜欢钻牛角尖,另一方面大多数学类库要么是科学计算多一些.要么偏娱乐性,要么精度低,要么算的慢.对我这专业都不合适.总是有点别扭的感觉.
再于是,介于我对汇编有点小研究,我决定使用汇编完成计算部分,用其他一些C++特性完成内存的分配,智能指针等等功能.我这领域的数学计算大致是:---准备工作,算半天,结束工作.准备和结束多慢都能接受,但中间的计算过程需要非常快速.这也是我打算使用汇编+ C++ 来完成的目的所在.各尽其职.
废话说完了,来讲讲我的经历... 汇编和C++混合嘛,就3条路,要么是inline汇编,要么是汇编编个C函数在C++里用extern "C"把函数包进去.再或者用编译器提供的Intrinsics来进行宏汇编.这三条路各有各的好处和坏处.
首先说说Intrinsics,这个东西之前我用了一段时间,其实是比较方便的,可以快速开发带有SSE或者AVX指令集算法程序.但是再深入一点的时候,发现这玩意能干的事情少了点.别的先不说,SSE指令相关的宏汇编并没有包含所有SSE指令,AVX就更不用说了,而且VC的编译器居然只支持到SSE2指令集,还不全.而且在实际使用中我发现这个东西的效率并不是=汇编效率.是,语句上看其实和汇编是一对一的,但是手写汇编还有一大好处是随意操作寄存器,有些方法的逻辑只有人能看出来,这使得优化能在汇编中进行地更彻底一点.另外,GC和VC的Intrinsics不大一样,跨平台很难搞,基本得一种方法写两遍.因此,Intrinsics----放弃!
其实我最推崇的还是用汇编生成静态库的形式,毕竟这样能让C++部分和汇编部分的"耦合度"降低一些.以前开发的时候我经常这么用,毕竟调用函数push和pop一下,只要__stdcall或者__fastcall这些东西设置对,基本不会出问题,以前总这样用,把软件数学算法部分汇编一写,别人拿走就用去了.比较爽.但是我之前写的,都是比较大的函数,比如一整套FIR滤波+频谱分析的完整算法,直接调用就行,要考虑的是cache missing的问题,而不是push和pop影响效率的问题.可现在如果是一个类库,那么必然一个大的方法是由一些简单的方法结合的,比如多个向量加法必然是一堆两个向量的加法合成的.这时如果每次向量加法都是一次函数调用,对性能影响还是不小的.一般这时如果是在汇编里,就用宏来搞就行了.(顺便推荐一下面向对象的汇编器HLA)但现在要做的是C++类库,总不能里面再包个汇编库吧...而且我对cache的尺寸老是拿不准,经常missing...因此我想到最好还是用内联汇编比较好可以把函数是否inline的权力交给编译器.当然,可以用extern inline来定义一个外部的inline函数,但神奇的是extern inline好像只能作用于外部的cpp函数或者c函数,一旦碰见外部汇编生成的静态库函数,iniline就失效了..VC和intel编译器都是如此,extren forcedinline也不行.
内联汇编的话,在x86里面比较简单,直接__asm{}就行了,比较恶心的在于:
1.VC居然不让人在x64下面内联汇编.
2.GCC的汇编语法和VC完全不一样.
3.VC对内联汇编的支持并不好.很容易出问题.
内联汇编语法的问题相信不少人都了解,GCC里用的是AT&T风格,VC里用的是intel风格.这个倒不是大问题,网上有这两种格式的转换器.AT&T看着比较晕但除了lea指令的写法比较恶心外其他的都还算合理.一般语句后要紧跟数据类型比如mov要写成movl之类,但我发现光写mov也可以,SSE相关指令比如movapd写了后缀反倒不能编译..不知是为何... 两种格式的问题主要在于,把代码放到GCC里面或VS里的时候,我总是要添加个宏判断一下编译器然后弄两个格式各来一个.又麻烦又容易出错(比如改了其中一个版本另一个忘了改).以前我一般都懒得写AT&T风格,如果代码需要用在mac里面,可以通过把编译器换成LLVM GCC来支持intel风格.至于x64下VS里的内联汇编,没有任何办法,有一些变通的办法但很恶心,目前我能找到的唯一办法就是用intel编译器了.intel编译器在VS里面用着一个比较方便,二个它算法方面优化相当好,三个MSVC支持的特性它都支持,甚至连predefined宏都和MSVC一样.总之就是很好.这几个问题中最要命的是:内联汇编不是C++标准的东西,编译器不知道你到底在asm{}块里面干了什么,它只是简单地把那段汇编代码贴到正确地位置上而已.这样主要会导致几个问题:首先是栈更容易被破坏,改一下esp直接就崩了.(VS还算不错,提醒你一下),另外是编译器不知道所有寄存器在asm{}块前后值的变化,其次是C代码和内联asm代码的衔接很要命.这个是我最关心的问题,比如一个整型变量在C++代码里的名字叫X,我当然可以简单地mov eax,X (intel风格)把它加载到eax寄存器里,这是一个内存读取操作,但实际上很多情况下会发现X的值根本就是在另外某个寄存器里,甚至本来就在eax里(由之前的C++代码导致的).这时根本就不需要这个mov指令.但关键问题是你不能肯定X到底在哪个寄存器里呆着.所以你还是mov eax,X最保险.然后你的最终代码里就会时不时带有类似这种恶心代码:
mov esi,X //这是编译器写出来的代码
mov eax,X //这是内联汇编你手写的代码
这是有病啊,纯有病...又或者asm{}块的输出写在了一个内存上,然后紧接着C++编译器又去那块内存上去读取变量了.生成的汇编代码和上面那个一样有病.再比如类方法,this指针一般存在ecx里传输,但你真不能肯定当前的ecx真的是this(千万别肯定).于是你又要从内存里读一遍this到某个寄存器里.当然,你可以把一个函数声明为裸函数(naked),然后从头至尾自己维护栈,寄存器等等.但是一旦一个函数裸了,它就不能内联了.......在这些问题的解决上,GCC做的相当漂亮,首先你可以把输入的值定义在一个虚拟的寄存器上,由编译器来决定具体用哪个寄存器(输出也一样),其次你可以用过手动告知编译器你这段asm()块改变了哪些寄存器的值,是否操作了内存.GCC甚至可以深入到asm块内去优化你写的汇编代码(前提是借助你告诉它的信息).这点在vc的内联汇编中是完全没有的.GCC生成的代码,反汇编的时候能看到内联asm和编译器生成的汇编语句的结合很舒服.以后有时间我会贴一些具体的代码出来.综上,最终我决定使用内联汇编的方式,用GCC的内联语法来写这个类库.wait,那VC怎么办? 简单,intel编译器同时支持intel汇编语法和AT&T语法(确实强).比如想在VC里用AT&T,先把编译器换成intel,再把VC默认的__asm{}这个语句,改为__asm__();你就可以在其中使用AT&T了.
另外再说个题外话,想在AT&T的格式的asm块中使用intel格式的内联汇编...可以这样:
asm (
".intel_syntax noprefix;"
"mov eax, _a;" //intel风格
"mov ecx, 3;"
"mul ecx;"
"mov _a, eax;"
"mov rax,rcx;"
"movupd xmm0,xmm2;"
".att_syntax noprefix"
);