转自:http://xiangxiaojiang.blog.163.com/blog/static/268961772007849414911/
缓式评估(lazy evaluation,延迟求值)是一项与语言无关的技术,它会延缓计算,直到不能再拖为止。工作的延迟当然不会带来性能的提升,但是如果被推迟的计算后来不再需要,从而不需执行的话,性能就提升了。也就是说免除了非必要的工作。
考虑一个用来代表员工(employee)的大型对象实例。这个对象内含许多值域,其中一些又是对象,内含与员工相关的各项数据。在创建这个对象的时候,需要指定独一无二的识别码作为数据库主键(key)。数据库内包含了创建这个对象所需的一切信息。请看Employee class及其相关的辅助类(support classes):
class NameInfo
{
private String lastName;
private String firstName;
private String middleName;
private String courtesyTitle;
//...
}
class ContactNumbers
{
private String homeNumber;
private String officeNumber;
private String mobileNumber;
private String faxNumber;
//...
}
class EmergencyContactInfo
{
private NameInfo name1;
private ContactNumbers number1;
private NameInfo name2;
private ContactNumbers number2;
//...
}
class AddressInfo
{
private String street;
private String city;
private String state;
private int zip;
//...
}
class WorkAddressInfo
{
private AddressInfo workAddress;
private String mailStop;
//...
}
class Employee
{
private NameInfo name;
private String jobTitle;
private String emailAddress;
private ContactNumbers phoneNumbers;
private EmergencyContactInfo emergencyContact;
private AddressInfo homeAddress;
private WorkAddressInfo workAddress;
public Employee(int employeeID)
{
//Make multiple queries to a database to gather the
//information about an Employee. Then build all of the parts
//of the Employee object with the data.
name = new NameInfo(...);
jobTitle = new String(...);
emailAddress = new String(...);
phoneNumbers = new ContactNumbers(...);
emergencyContact = new EmergencyContactInfo(...);
homeAddress = new AddressInfo(...);
workAddress = new WorkAddressInfo(...);
//...
}
public AddressInfo homeAddr()
{
return homeAddress;
}
//...
}
如上所示,在创建Employee对象之前,需先读取数据库,并先创建数个聚合对象(aggregate objects)。Praxis32讨论过对象创建的代价。
假设当创建Employee对象时并不需要用到相关所有数据。例如当写下这段代码:
public void printHomeAddress(int employeeID) {
Employee emp = new Employee(employeeID);
AddressInfo addrinfo = emp.homeAddr();
System.out.println("Street " + addrinfo.street());
System.out.println("City " + addrinfo.city());
System.out.println("State " + addrinfo.state());
System.out.println("Zip " + addrinfo.zip());
}
这段代码支付了创建Employee对象的全部开销,但是在超出函数作用域(method scope)之前,它只使用了一个值域(就是homeAddress)。我们的目标是降低对象的创建成本,那么,消减这一类开销的途径之一就是利用缓式评估(延迟求值)技术,令Employee对象的创建动作更廉价。
通过缓式评估技术,让我们将各种计算推迟到最后必要关头。对Employee而言,意味不在其构造函数中建构完整对象,而是先将所有值域(都是object reference)初始化为null。当对一个Employee对象调用其成员函数时,这时候才建构出对象的必要部分以满足函数调用。运用这样的思路,Employee及其构造函数修改如下:
class Employee
{
private NameInfo name;
private String jobTitle;
private String emailAddress;
private ContactNumbers phoneNumbers;
private EmergencyContactInfo emergencyContact;
private AddressInfo homeAddress;
private WorkAddressInfo workAddress;
private int employeeID;
public Employee(int eid)
{
employeeID = eid;
}
//...
}
在创建Employee对象时,构造函数的惟一功能就是在class的instance数据中存储employeeID值。这使得Employee对象的创建速度提升不少。其他的instance数据都被设置成null——这不需要构造函数亲自动手,因为null是这些值域的缺省值。在初始化某个class时,JVM会在调用构造函数本体之前先将所有instance变量设置为相应缺省值(参见Praxis37)。
对构造函数做了这样的修改之后,Employee的函数也需要改动。例如homeAddr()做了如下修改:
public AddressInfo homeAddr()
{
if (homeAddress == null)
{
//query database based on stored employeeID and gather
//information to create the AddressInfo object.
}
return homeAddress;
}
这样,通过采用缓式评估技术,不再需要收集用不上的数据,从而得以节省打量时间。于是Employee对象的创建工作得以从非常昂贵变得非常低廉。
这项技术同意可以运用于数字计算。某些计算格外花费时间,通过缓式评估(延迟求值)的巧妙运用,往往可以显著加速程序的执行速度。考虑下面这个为矩阵(matrix)设计的class,其中含有矩阵相加函数和相乘函数:
class Matrix2D
{
private int[][] matrix;
public Matrix2D(int sizex, int sizey)
{
matrix = new int[sizex][sizey];
}
public void addMatrix(Matrix2D other)
{
//add other to this.
}
public void multiplyMatrix(Matrix2D other)
{
//multiply other by this.
}
public int elementAt(int row, int col)
{
//return element at row/col
}
//...
}
下面是这个class的一些用法:
Matrix2D matrix1 = new Matrix2D(500,500);
Matrix2D matrix2 = new Matrix2D(500,500);
matrix1.multiplyMatrix(matrix2);
matrix1.elementAt(275, 314);
这段代码创建出两个矩阵,将二者相乘,然后取回250000个元素中的一个。如果其他249999个元素在“结果矩阵”超出生存空间(作用域,scope)之前没有被用到,两个矩阵的相乘工作就几乎白费了。即便有四分之三的矩阵元素被访问,仍然浪费了62500个相乘动作。这意味着CPU资源的巨大浪费。
较好的解决方案是采用缓式评估技术。在multiplyMatrix()或addMatrix()执行的时候,不对矩阵进行完全的相加或相乘。不,什么都不做,之保存“施加于矩阵身上之动作形式”的相关信息。然后,当程序以elementAt()取出矩阵某些区域的时候,才去计算(和返回)必要数据。这种方式只做必要工作,略去无谓劳动。下面是实现这项技术的一个范例:
class Matrix2D
{
private int[][] matrix;
private boolean performAdd;
private boolean performMultiply;
private Matrix2D mat;
private boolean actionPending;
private int width;
private int height;
public Matrix2D(int sizex, int sizey, int initialValue)
{
width = sizex;
height = sizey;
matrix = new int[sizex][sizey];
for (int i=0; i<sizex; i++)
for (int j=0; j<sizey; j++)
matrix[i][j] = initialValue;
}
public void addMatrix(Matrix2D other)
{
if (actionPending)
performAction();
performAdd = true;
performMultiply = false;
mat = other;
actionPending = true;
}
public void multiplyMatrix(Matrix2D other)
{
if (actionPending)
performAction();
performMultiply = true;
performAdd = false;
mat = other;
actionPending = true;
}
public int elementAt(int row, int col)
{
int value;
if (performAdd)
value = (mat.matrix[row][col]) + (this.matrix[row][col]);
else
value = (mat.matrix[row][col]) * (this.matrix[row][col]);
actionPending = false;
return value;
}
private void performAction()
{
if (performAdd)
{
for (int i=0; i<width; i++)
for (int j=0; j<height; j++)
matrix[i][j] += mat.matrix[i][j];
}
else if (performMultiply)
{
for (int i=0; i<width; i++)
for (int j=0; j<height; j++)
matrix[i][j] *= mat.matrix[i][j];
}
}
//...
}
上述代码只是为了说明这项技术的使用,因此省略了一些错误检查。注意,这段代码在一个时刻只保留一个动作轨迹。例如,假设你创建了两个Matrix2D对象。第一个对象调用addMatrix()并传入第二个矩阵对象。在这次调用中,addMatrix()仅仅只是存储“矩阵相加”信息。如果在调用elementAt()之前调用了addMatrix()或multiplyMatrix(),则先执行“未决动作”(pending action),而后才存储新动作(的相关信息)。如果希望程序更加高效,则可以添加额外代码来存储更多的未决动作。
采用这项技术可以减少计算量,带来明显的性能收益。但是请记住,缓式评估(延迟计算)技术并非总是能够带来更快速的代码。以上述矩阵为例,如果矩阵的所依数值都将被访问,那么缓式评估技术反而会使程序速度迟缓,因为这项技术需要额外的数据和逻辑。只有在“为满足性能目标,你可以免除够多的非必要计算”情况下,缓式评估才是有益的。其基本策略就是“延缓那些很可能永远也不需要进行的工作”。