您的位置:首页 > 运维架构

有关primitive operation的参考说明

2005-10-16 22:02 141 查看
这几天收到了一些有关primitive operation计算的问题,问题主要集中在如何判定
一个操作是否属于primitive operation(以下简称PO),以及与方法调用有关的PO计
算上.PO作为算法评估的一个基本概念,从本质上讲就是简单的操作指令,注意这里提
到操作指令是指机器能够直接接受的操作指令,大家可以把它与汇编语言作类比,但
从认识角度上来讲,PO比汇编稍微高级一点。以下将给出我个人对PO的一些理解,
仅供参考,希望对大家理解PO有所帮助。

另外对于同学所寄来的问题,我不作个别回答,希望大家能够在这篇参考说明
中找到所需的答案。

在REM的PPT中提到PO由以下几类操作

Assigning a value to a variable
任何赋值语句都是PO,相当于汇编 mov A B

EG: a = 1; // 1 PO

Calling a method
调用一个方法:call methodAddress(将返回地址压栈)
但是,这里仅仅指调用方法,方法内部的有效运算还得另外算PO数。
由此,大家可以看到调用方法越多,是会产生越多的执行开销(overhead)。

这里有一个简单的例子:
Program 1:
statement1; // 1 PO
statement2; // 1 PO
// total 2 PO

Program 2:
function1(); // 1 PO
// total 4 PO

function1(){
statement1; // 1 PO
statement1; // 1 PO
// return // 1 more PO, 即便没有写,隐式也会有一个 return
}

所以不要认为把复杂的算法包装在方法中就能降低计算复杂度,
这样做只能帮倒忙!!!

Performing an arithmetic operation
任何一次计算都要被算成一次 PO, 因为CPU一次时钟循环只能处理一个运算指令,
有时CPU的时钟循环连一个运算都完不成(比如说乘法和除法)。
add A B, sub A B, mul ...etc

EG: a + b * ( c - d ); // 3 PO

Comparing two numbers
比较运算: test A B, ...etc
显然每一次值比较,都要算一个PO

但或许大家也能举出一个“反例”
int a[] = {1};
if(a[0] == 3) {...} // 1 + 1 PO,注意这里取a[0]值的时候还计一次PO

大家也可以想想看,下面例子中Program 3的PO总值是多少?

Progam 3:
int i; // 注意声明一个变量是不计 PO 的。
if(getValue() <= 5) {
i--;
}

function2(){
return 4;
}

Indexing into an array
Following an object reference
以上两种情况,在底层的语言上来看是一样,如果用C语言来描述就是一个指针取值
的过程:
A[5] ::= *(a + 5)
// 虽然看似有两个操作,但在汇编层面上却只有一条取值指令。
B.field ::= *(B + offsetOf(field, B))
// 做的好的编译器,会把field在B中的offset算出来,把它当作常量编入指令,
// 所以姑且也能算作一个 PO

Returning from a method
从一个方法返回:ret(将调用call时压栈的地址弹出,
并将当前指令位置指向该值)

即便源代码中方法段中没有写return,编译器也会主动地位为他加上,否则程序的
执行流就无法返回到原先 call 这个函数的位置。

一个比较有意思的意思就是
function4() {;} // implicit return
虽然这个函数什么也没做,但一调用,就耗了 2 PO。

特殊说明:
最差估计
如果一个程序有多个分支(if),那么对这个程序的算法复杂度计算进行最差
估计的话,就要取 PO 数最大的那条路径(以下称critical path)。

EG:
// a[0] 个 branch
if(boolval_1_1){
}
else if(boolval_1_2){
}
...
else if(boolval_1_a_0s)
else{
}
// a[1] 个 branch
...
...
// a[m] 个 branch
if(boolval_m_0){
}
...
else{
}

那么,一共会有a[0]*a[1]*...*a[m]条path,大家都学过排列组合,结果这么
来的,就不解释了。
在计算 O(f(n))时,就要取这些path中 PO 数最大的 path,
O(f(n)) ::= O( Big-Oh of critical path )
在计算 Ω(f(n))时,就要取这些path中 PO 数最下的 path,
Ω(f(n)) ::= Ω( Big-Omega of fastest path )
在计算 Θ(f(n))时,就要采用均摊分析了,
这个属于高级话题,这里就不提了。

对于循环来说,问题相对比较简单,大家只要对循环执行次数估计正确就可以了。
O(f(n))时,取循环最大次数。
Ω(f(n))时,取循环最小次数。
Θ(f(n))时,取循环平均次数。

补充解释:
For的header:
for i = 1 to n - 1 do // 1 + 2n + 2(n - 1) operations
1 -- i = 1 赋值
2n -- n * 2 循环测试会被执行 n 次,
每次执行 i <= n - 1,如果是true,则继续循环。
2(n - 1) -- 对 i 值的更新(i++)会被执行 n - 1 次,
一共会有 n 次,程序流会回到 for header,
第一次是把 1 赋给 i,剩余 n - 1 次更新 i。(i++)

所以把REM的式子写成 (1*1 + 2*(n -1)) + 2n,或许更容易理解。

new的本质:
java中 new 的操作,实际上是一个复合操作:
new A ::= malloc(sizeof(A)); // 分配足够的内存
A() // 调用构造器

正是由于这种复杂性(我们无法得知JAVA类库怎么实现),
所以我们目前可以订一个约定:
任何 new 和库函数操作,就抵 3 OP。

// 在Practical 1中,本约定不作数,从Practical 2开始生效。

END.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: