More Effective C ++ Terms 17

zhaozj2021-02-08  280

Terms 17: Consider using Lazy Evaluation (lazy calculation)

From the point of view of efficiency, the best calculation is not calculated at all, that's good, but if you don't have to calculate it at all, do you also add the code to join the code at the beginning of the program? And if you don't need to calculate, then How do you have to execute these code?

The key is to be lazy.

Remember? When you are still a child, your parents call you to organize the room. If you like me, you will say "good" and continue to be your own business. You will not go to the room. In your heart, I have been arranged in the final position. In fact, until you hear the parents to the hall to see if your room has been organized, you will run into your room and start finishing the fastest speed. . If you are lucky, your parents will not check your room, so you can don't have to organize the room at all.

The same delay strategy is also applicable to the work of C programmers with five years of working age. In computer science, we respect such delays for lazy evAolization. When you use Lazy Evaluation, classes that use this method will postpone calculation work until the system needs these calculations. If no results need to be calculated, the customer's customers are not so smart like your parents.

Maybe you want to know what I mean by these. Maybe you can help you understand. Lazy Evaluation is widely used in a variety of applications, so I will tell four parties.

Quote count

Class string {...}; // a String class (The Standard

// String Type May Be IMplement

// AS described Below, But it

// Doesn't Have to BE)

String S1 = "Hello";

String S2 = S1; / Call String Copy Construction Function

Usually the String copy constructor allows S2 to be initialized by S1, S1 and S2 have their own "hello" copy. This copy constructor causes a large overhead because the copy of the S1 value is to be made, and the value is assigned to S2, which typically needs to dispense the stack memory with the New operator (see Terms 8), you need to call the strcpy function copy S1 Data to S2. This is an EAGER EVALAN: Just because of the String copy constructor, the copy of the S1 value is to be made and it is assigned to S2. However, S2 at this time does not require copy of this value because S2 is not used.

Lazy energy is less work. There should be a copy of the S2 S1, but the S2 is shared with S1. We only have to do some records to know who is sharing, it can save the overhead of the calling new and copy characters. In fact, S1 and S2 share a data structure, which is transparent for Client. For the following example, this is nothing difference because they just read data:

Cout << S1; // Read S1 value

COUT << S1 S2; // Read S1 and S2 value

The method of sharing the same value is only different when this or the value of the String is modified. Only modify the value of a string, rather than two are modified, this is extremely important. For example this statement:

s2.converttouppercase ();

This is a pool, only the value of S2 is modified, not a modification of the value of S1. In order to perform the statement, the String's CONVERTTOUPERCASE function should make a copy of the S2 value, and assign this private value to S2 before modifying. In ConvertTOUPPERCASE, we can't be lazy: Must make copies for S2 (shared) values ​​to let S2 use themselves. On the other hand, if S2 is not modified, we don't have to make the copy of its own value. Continue to keep the shared value until the program exits. If we are lucky, S2 will not be modified. In this case, we will never consume energy for the independent value.

Detail of this shared value method (including all code) is provided in terms 29, but its contained principle is LAZY Evaluation: unless you need, don't make a copy of anything. We should be lazy, as long as you may share other values. In some applications, you can often do this.

Difference to reading and writing

Continue to discuss the above REFERENCE-Counting String object. Take a look at the second method using Lazy Evaluation. Consider this code:

String s = "homer's iliad"; // hypothesis is one

// Reference-Counted String

...

Cout << S [3]; // Call Operator [] Read S [3]

s [3] = 'x'; // Call Operator [] Write S [3]

First call Operator [] to read the String part of the String, but the second call is to complete the write operation. We should be able to distinguish between read calls and write calls, because reading the Reference-Counted String is easy, and writing this String requires a new copy to the String value before writing.

We can't fall into difficulties. In order to be able to do so, different measures need to be taken in Operator [] (the function is called if the function is called to complete the read operation or to complete the write operation). If we determine whether the CONTEXT calling operator [] is the reading operation or write operation? The cruel fact is that we can't judge. By using the Proxy Class described in Lazy Evaluation and Terms 30, we can postpone the decision to make a read operation or write, until we can determine the correct answer.

Lazy fetching (lazy extraction)

Example of the third Lazy Evaluation, assume that your program uses some large objects that contain many fields. The survival of these objects exceeds the program period, so they must be stored in the database. Each pair has a unique object identifier to re-obtain the object from the database:

Class LargeObject {// Large lasting object

PUBLIC:

LargeObject (ObjectID ID); // Recover the object from disk

Const string & field1 () const; // field 1 value

INT Field2 () const; // field 2 value

Double Field3 () const; // ...

Const string & field4 () const;

Const string & field5 () const;

...};

Now consider restoring LargeObject over the disk:

Void RestoreandProcessObject (ObjectID ID)

{

LargeObject Object (ID); // Restore Object

...

}

Because the LargeObject object instance is very large, all data is obtained for such objects, and the overhead of the database will be very large, especially if data is acquired from the remote database and send data over the network. In this case, you don't need to read all the data. For example, consider such a program:

Void RestoreandProcessObject (ObjectID ID)

{

LargeObject Object (ID);

IF (Object.field2 () == 0) {

COUT << "Object" << ID << ": null field2./n";

}

}

It only requires the value of Filed2, so efforts to get other fields are wasteful.

When the LargeObject object is established, all data is not read from the disk, so that the lazy method solves this problem. However, this data is only an object "shell" that is required, and this data is retrieved from the database when a data is needed. The implementation method of this "Demand-Paged" object initialization is:

Class largeObject {

PUBLIC:

LargeObject (ObjectID ID);

Const string & field1 () const;

INT FIELD2 () Const;

Double Field3 () Const;

Const string & field4 () const;

...

Private:

Objectid OID;

Mutable string * field1value; // See below

Mutable Int * Field2Value; // "Mutable" discussion

Mutable Double * Field3Value;

Mutable string * field4value;

...

}

LargeObject :: LargeObject (ObjectID ID)

: OID (ID), Field1Value (0), Field2Value (0), Field3Value (0), ...

{}

Const string & largeObject :: field1 () const

{

IF (Field1Value == 0) {

Read data from the database for FileD 1,

Field1Value points to this value;

}

Return * Field1Value;

}

Each field in the object is represented by a pointer to the data, the LargeObject constructor initiates each pointer into empty. These empty pointer indicates that the field has not read the value from the database. Each LargeObject member function must check the status before the data pointed to by the field pointer. If the pointer is empty, the corresponding data must be read from the database before operating the data.

When Lazy Fetching is implemented, you face a question: In any member function, you may need to initialize the null pointer to point to real data, including in the Const member function, such as Field1. However, when you try to modify the data in the Const member function, the compiler will have problems. The best way is to declare the field pointer to mutable, which means that they can be modified in any function, even in the Const member function (see Effective C Terms 21). This is why the field declares the field as Mutable in LargeObject. Keyword Mutalbe is a relatively new C feature, so your compiler may not support it. If so, you need to find another way to allow your compiler to allow you to modify data members in the Const member function. One way is called "fake this" (forged this pointer), you create a point to the Non-Const pointer, the point to which is the same. When you want to modify data members, you access it through "Fake this":

Class largeObject {

PUBLIC:

Const string & field1 () const; // has no change

...

Private:

String * field1value; // does not declare to Mutable

... // Because the old compiler is not

}; // support it

Const string & largeObject :: field1 () const

{

// Declare the pointer, FakeThis, which points to the same object

/ / But the constant attribute of the object has been removed

LargeObject * const fakethis =

Const_cast ;

IF (Field1Value == 0) {

FAKETHIS-> Field1Value = // This assignment is correct,

The appropriate data // Because FakeThis points to

From the database; // Object is not Const

}

Return * Field1Value;

}

This function uses const_cast (see Terms 2) to remove the const property of * this. If your compiler does not support COSNT_CAST, you can use the vintage C style Cast:

// Use old-fashioned CAST to imitate Mutable

Const string & largeObject :: field1 () const

{

LargeObject * const fakethis = (largeObject * const);

... // AS ABOVE

}

Look at the pointer in LargeObject, you must initialize these pointers as empty, and then test each time you use them, which is troubles and easier to cause errors. Fortunately, using the Smart pointer to automatically accomplish this bitterness, details can be found in Terms 28. If you use a Smart pointer in LargeObject, you will also find that you no longer need to declare pointers with Mutalbe. This is just temporary because you will eventually touch Mutalbe when you implement the Smart pointer class.

Lazy Expression Evaluation About the last example of Lazy Evaluation comes from digital programs. Consider this code:

Template

Class Matrix {...}; // for Homogeneous Matrices

Matrix m1 (1000, 1000); // a matrix of 1000 * 1000

Matrix M2 (1000, 1000); //

...

Matrix m3 = m1 m2; // m1 m2

Usually the implementation of Operator uses Eagar Evaluation: In this case, it calculates and returns the sum of M1 and M2. This calculation amount is quite large (100,000 times additional operation), and of course the system also assigns memory to store these values.

Lazy Evaluation Method said that there is too much work, so don't do it. Instead, a data structure should be established to indicate that the value of M3 is M1 and M2, and it is an addition operation with an ENUM. Obviously, the establishment of this data structure is much faster than M1 and M2, and a large amount of memory can also be saved.

Consider the part of the program, before using M3, the code is executed as follows:

Matrix M4 (1000, 1000);

... // Assignment to M4

M3 = m4 * m1;

Now we can forget that M1 is M1 and M2 and (so saving the cost of calculation), where we should remember that M3 is the result of M4 and M1 operations. Don't say, we don't have to make a multiplication. Because we are lazy, remember?

This example looks some doing, because a good programmer does not write this procedure: calculate the two matrices and do not use them, but it is actually not the same as it looks. Although the programmer does not perform unwanted calculations, the programmer in maintenance modifies the path path, making the previous useful calculations have no effect, which is common. By defining the objects that calculate before use can reduce the likelihood of this situation (see Effective C Terms 32), this problem occasionally occasionally occurs.

But if this is the only time to use Lazy Evaluation, it is too not worth it. A more common application area is when we only need to calculate a part of the results. For example, suppose we initialize the value of M1 of M1 and M2, and then use M3 like this:

Cout << m3 [4]; // Print the fourth line of M3

Obviously, we can't be lazy, and should calculate the fourth line value of M3. But we can't be a big ambition, we have no reason to calculate the results other than the M3 fourth line; the remaining parts of M3 still maintain uncapsulated state until they do need. Very luck, we have never needed.

How can we walk so? The experience in the field of matrix computing shows that this possibility is very large. In fact, Lazy Evaluation exists in the APL language. The APL is developed in the 1960s and can perform matrix-based interactive operations. At that time, the computing capacity of Hou running its computer has not yet now highlights the chip in the microwave, and the matrix can be performed on the surface of the APL. Its skill is Lazy Evaluation. This technique is usually valid because the user plus, multiply or divided by the general APL is not because of the value of the entire matrix, but only a small portion of the value. The APL uses Lazy Evaluation to delay their calculations until it knows which part of the required matrix is ​​required, and then only calculates this part. In fact, this allows users to interact with a large amount of calculations on a computer that cannot complete Eager Evaluation. Now the computer speed is very fast, but the data set is also larger, and the user is also patient, so many of the current matrix programs are still using lazy evAluation. Telling fairly, lazy sometimes fail. If you use M3 like this:

Cout << m3; // Print M3 all values

Everything is over, we must calculate all the values ​​of M3. Also if you modify any matrix on M3, we must also calculate immediately:

M3 = m1 m2; // Remember M3 is M1 and M2 and

//

M1 = m4; // Now M3 is the old value of M2 and M1!

//

Here we must take steps to ensure that the assignment will not change M3 after assignment. In the Matrix assignment operator, we can capture the value of M3 before changing M1, or we can make a copy of the old value of M1 to make M3 depend on this copy calculation, we must take steps to ensure that M1 is assigned The value of M3 remains unchanged. Other functions that may modify the matrix must be handled in the same way.

Because two values ​​are required to store two values, maintain storage values, dependencies, or both, overload operators such as assignment, copy operation, and addition, Lazy Evaluation is used in digital fields. On the other hand, it runs a program that often saves a lot of time and space.

to sum up

These four examples show that Lazy Evaluation is useful in all areas: avoids unwanted object copies, distinguishing between read operations by using Operator [], avoid unwanted database read operations, avoid unwanted digital operations . But it is not always useful. Just as your parents always come to check your room, then delays will not reduce your workload. In fact, if your calculations are important, Lazy Evaluation may slow down speed and increase memory, because in addition to all calculations, you must maintain the data structure to operate as much as possible in the first time . In some cases, the software is required to be avoided, and Lazy Evaluation is useful.

Lazy Evaluation has no special things for C . This technology can be used in a variety of languages, such as famous APLs, Dialects of Lisp (in fact all data stream languages) make this idea as a basic part of language. However, mainstream programming languages ​​use EAGER Evaluation, C is a mainstream language. However, C is particularly suitable for users to implement Lazy Evaluation, as it supports the packaging to add Lazy Evaluation in the class, while do not need to let the classes of the class know. Look at the code snippet in the above example, you can know that Eager is still lazy evAluation, which is not half a difference in Interface in the class. That is to say, we can use the Eager Evaluation method to implement a class, but if you use the Profiler survey (see Terms 16) to display a performance bottleneck, you can use the class of Lazy Evaluation to replace it (see Effective C Terms 34). It is only the improvement of performance (after recompile and link) for Client. This is the case of Client like software upgrades, which allows you to be proud of lazy.

转载请注明原文地址:https://www.9cbs.com/read-1007.html

New Post(0)