Chapter 13. Applet Optimization

Smart cards represent the smallest computing platform in use today. A major factor influencing the design and implementation of Java Card applets is the limited availability of computing resources (data and program memory and CPU cycles) in the smart card environment.

This chapter focuses on applet optimizations. It provides a number of recommendations that you, the applet developer, can apply in designing and implementing applets. In many cases, a discussion is provided with the recommendation to help you understand various design trade-offs.

Optimizing an Applet's Overall Design

When optimizing an applet, you should first review the applet's overall design to identify any optimization opportunities. The reason is that optimizations can usually be applied at the global level and thus provide the most payoff.

To optimize an applet's overall design, you should apply the object-oriented features of the Java language. A Java Card applet consists of one or more classes. A class provides an organized way to define the variables and methods common to all objects of a certain kind. Thus, classes provide the benefits of modularity and reusability. Modularity and reusability can be further achieved by using inheritance. Inheritance allows you to define generic and common behavior in a superclass and to add specialized behavior to each subclass. When defining classes and their inheritance hierarchy, you should consider all applets coexisting in a card and decide whether common functions can be reused.

Optimization is often a balancing act. To optimize an applet's overall design, you should also consider the memory limitations of smart cards. Each class requires a data structure to represent it, and this involves overhead. Class inheritance also adds overhead, particularly when the inheritance hierarchy becomes complex. Therefore, an applet whose architecture maximizes flexibility through a greater number of classes and methods, nonoptimized data representation, and multiple levels of indirection creates significant overhead. On the other hand, a compact applet architecture with fewer classes and methods, packed data representation, and hard-coded logic might consume less memory. But such an architecture limits opportunities for interapplet code sharing. Also, debugging and updating such applet code become very difficult. There is no clear measure to indicate what approach you should take and how far you should go. Applet developers need to balance the design choices based on the applet's requirements and how the applet will be used. Typically, an applet would have between 5 and 10 classes and should have no more than two or three levels of class inheritance.

On-Card Execution Time

In a smart card system, cryptographic operations and EEPROM writes account for most of the on-card processing time. Therefore, it is usually not feasible to deploy computationally expensive cryptographic operations on voluminous data. For example, RSA is not commonly used for data encryption in smart cards, despite its very good security, due to the long execution time it requires. Its main application is in digital signatures, which are computed from encrypting hash values (small “fingerprints” of larger sets of data)[1].

To minimize EEPROM writes, transient arrays (in RAM) can be used to store intermediate results or frequently updated temporary data. Writing to RAM is 1,000 times faster than writing to EEPROM. Also, RAM is not subject to wear as is EEPROM. However, RAM is a very scarce resource on a card. Its use should be economized in every possible way.

Method Invocations

During execution, the Java Card virtual machine uses a data structure in RAM, called a stack, for holding method parameters, return values, local variables, and partial results. In many cards, the size of the stack is around 200 bytes. Therefore, to reduce the stack usage, you should optimize the use of method parameters and local variables. You should also limit nested method invocations, which could easily lead to stack overflow. In particular, applets should not use recursive calls.

Creating Objects in Applets

As explained in Chapter 7, an applet is created when its install method is called. The install method uses new to instantiate an applet instance. It is recommended, that, when possible, an applet should create in its constructor all objects it will need in its lifetime. This has two advantages. First, the applet can budget space to avoid an out-of-memory problem during execution. Second, the install method is enclosed within a transaction. In this way, should the applet installation fail due to memory allocation or other problems, the space allocated by the applet can be reclaimed. Needless to say, an applet may need to create more objects when it runs. An applet should always check that an object is created only once.

In any case, when a new object is created, the applet should assign the object reference to a permanent location, such as a class field (static field) or an object field (instance field) or a permanent array element. In this way, the object can be used throughout the applet's lifetime.

Reusing Objects

In the Java platform, objects are created as needed and are garbage collected when they are not referenced by other objects or by the virtual machine. When the virtual machine terminates, all objects are destroyed.

In a Java Card implementation, garbage collection may not be supported. In such an implementation, both persistent objects (allocated in EEPROM) and transient objects (allocated in RAM) exist throughout the lifetime of the card. Therefore, applets should not instantiate objects with the expectation that their storage will be reclaimed. For example, in the following code fragment, an object is created whenever the method is invoked. Such an object is singly referenced by a local variable and becomes unreachable after the method returns. Sooner or later, these dangling objects would consume all memory space and make the card unusable.

public void myMethod() {

   Object a = new Object();
}

Therefore, in the Java Card platform, the general rule is that a single instantiation of an object should be reused by writing new values to the member variables. This model is different from the one you may be accustomed to in Java technology.

The exception classes in the Java Card APIs are implemented in a manner that maximizes object recycling by using the system instance each time the method throwIt is invoked:

public class ISOException extends CardRuntimeException {

   private static ISOException systemInstance;

   // called once by the JCRE to create a system instance.
   // this instance is shared by all applets.
   public ISOException (short sw) {

      super(sw);
      if (systemInstance == null)
         systemInstance = this;
   }

   public static void throwIt(short sw) {

      systemInstance.setReason(sw);
      throw systemInstance;
   }
}

The throwIt method is declared static so that it can be invoked by any applet. When calling the throwIt method, an applet customizes the ISOException object with a reason code:

if (apdu_buffer[ISO7816.OFFSET_P1] != 0)
   ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);

Eliminating Redundant Code

Eliminating redundant code is an often-used and effective optimization technique. Redundant code exists when two or more segments of code duplicate an identical function. An optimization can be achieved when redundant segments of code are factored out into a separate method.

To increase reusability and modularity (and thus eliminate code redundancy), programmers are often encouraged to write small methods with only 10 to 15 lines of code. However, a trade-off exists in eliminating redundant code. Creating more methods adds extra overhead, including the data structures to represent the methods, and method invocation bytecodes. Therefore, factoring the code to eliminate redundancy may actually increase the overall code size.

Applet developers should identify code segments that really warrant separation for the purpose of reusability. On the one hand, small methods possibly should be inlined, especially if invoked only once by the applet. On the other hand, if the size of the redundant code is large enough and such code repeats in several places, factoring it into a separate method can save a considerable amount of memory.

Accessing Arrays

Typically, accessing array elements requires more bytecodes than accessing local variables. To optimize memory usage, if the same element of an array is accessed multiple times from different locations in the same method, save the array value in a local variable on the first access, and then use the variable in subsequent accesses. Consider the following code example in the wallet applet's process method:

public void process(APDU apdu) {

   if (buffer[ISO7816.OFFSET_INS] == VERIFY)
      verifyPIN(apdu);
   else if (buffer[ISO7816.OFFSET_INS] == CREDIT)
      credit(apdu);
   else if (buffer[ISO7816.OFFSET_INS] == DEBIT)
      debit(apdu);
   else if (buffer[ISO7816.OFFSET_INS] == CHECKBALANCE)
      checkBalance(apdu);
   else if (ins == UPDATEPIN)
      updatePIN(apdu);
   else
      ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}

The element of array buffer indexed at ISO7816.OFFSET_INS is accessed and checked repeatedly to determine what task is specified in the APDU command and thus which service function to invoke. This code can be optimized to save memory if the indexed array element is cached in a local variable:

public void process(APDU apdu) {

   byte ins = buffer[ISO7816.OFFSET_INS]; // cache it

   if (ins == VERIFY)
      verifyPIN(apdu);
   else if (ins == CREDIT)
      credit(apdu);
   else if (ins == DEBIT)
      debit(apdu);
   else if (ins == CHECKBALANCE)
      checkBalance(apdu);
   else if (ins == UPDATEPIN)
      updatePIN(apdu);
   else
      ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}

To ease array manipulation in applets, the class javacard.framework.Util provides a number of convenient methods. The arrayCompare method allows you to compare the elements of two arrays, with a return value indicating whether one array is less than, equal to, or greater than the other.

The arrayCopy method copies an array to another location. This method ensures that the copy operation is transactional, which means that an error that occurs in the middle of the operation does not result in a partially copied array. But the arrayCopy method requires more EEPROM writes and thus takes longer execution time. If the data in the destination array need not be preserved in case of an error, you should instead use the nonatomic version of the same method, arrayCopyNonAtomic. A similar method, arrayFillNonAtomic, nonatomically fills the elements of a byte array with a specified value.

Two consecutive byte array elements may be returned as a short value using the getShort method. Likewise, two consecutive byte array elements may be set using the first and second bytes in a short value using the setShort method.

The switch Statement versus the if-else Statement

The nested if-else statements in the wallet's process method (page 188) can be transformed to an equivalent switch statement:

public void process(APDU apdu) {

   byte ins = buffer[ISO7816.OFFSET_INS];
   switch (ins) {
      case VERIFY: verifyPIN(apdu); break;
      case CREDIT: credit(apdu); break;
      case DEBIT:  debit(apdu); break;
      case CHECKBALANCE: checkBalance(apdu); break;
      case UPDATEPIN: updatePIN(apdu); break;
      default: ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
   }
}

In general, a switch statement executes faster and takes less memory than an equivalent if-else statement. But in some cases, a switch statement might actually take up more memory. So you should try it both ways and use empirical evidence to determine which statement is more efficient.

When writing an applet, there are many optimization opportunities in which you can reconstruct a cascaded if-else statement or a switch statement to reduce code size. For example, in the wallet applet, assume that the PIN must be verified before other functions can be executed:

byte ins = buffer[ISO7816.OFFSET_INS]; // cache it

if (ins == VERIFY)
   verifyPIN(apdu);
else if (ins == CREDIT)
   if (isPinValided() == false)
      throw ISOException(SW_PIN_NOT_VALIDATED);
   credit(apdu);
else if (ins == DEBIT)
   if (isPinValided() == false)
      throw ISOException(SW_PIN_NOT_VALIDATED);
   debit(apdu);
else if (ins == CHECKBALANCE)
   if (isPinValided() == false )
      throw ISOException(SW_PIN_NOT_VALIDATED);
   checkBalance(apdu);
else if (ins == UPDATEPIN)
   if (isPinValided() == false)
      throw ISOException(SW_PIN_NOT_VALIDATED);
   updatePIN(apdu);
else
   ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);

Notice that the code checking whether the PIN has been validated is duplicated in a number of places. Memory can be saved if the duplicated code can be eliminated. Compare the preceding code with the following:

byte ins = buffer[ISO7816.OFFSET_INS]; // cache it

if (ins == VERIFY)
   verifyPIN(apdu);
else {
   if (isPinValided() == false)
      throw ISOException(SW_PIN_NOT_VALIDATED);

   if (ins == CREDIT)
      credit(apdu);
   else if (ins == DEBIT)
      debit(apdu);
   else if (ins == CHECKBALANCE)
      checkBalance(apdu);
   else if (ins == UPDATEPIN)
      updatePIN(apdu);
   else
      ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}

Factoring out common code and relocating it to a commonly accessible area eliminates redundancy in a method. This scheme is particularly useful in an applet's process method, in which selections of service functions are coded as cascaded if-else or switch statements that check incoming APDUs.

Arithmetic Statements

You might think that a compound arithmetic statement instead of separate assignments would use fewer bytecode instructions. The truth is quite opposite: a compound statement is actually more efficient.

x = a + b; x = x - c;
x = a + b - c;

The reason is that separate assignments require additional bytecode instructions to first store the intermediate value (a + b) in a variable and then load it back onto the stack for the next calculation.

Although it is a good idea to combine two or three operations into a single compound statement, a compound statement with too many levels of nested arithmetic operations can degrade the readability of source code and can be error prone.

Optimizing Variables in Applets

Variables in a Java Card applet can be of various kinds: instance variables, class variables, local variables, or method parameters. Local variables must be initialized before they can be accessed. Initialization is enforced by the Java compiler. In contrast, if no initializers are defined for instance or static variables in an applet, the virtual machine automatically applies the default initializers, setting their values to zero, false, or null. Therefore, you do not need to write code to initialize instance and static variables in an applet if they need no preassigned values other than the default ones (see the following code). This is different from programming in C, in which a variable might contain an arbitrary value if not explicitly initialized in the code.

public class MyApp extends Applet {
   // no initialization is needed for the following fields
   static int a = 0;
   boolean b = false;
   byte[] buffer = null;
}

Static fields (class variables) can be declared either final or nonfinal. Final static fields are used to represent constants. In the Java Card system, static fields (both final and nonfinal) may be initialized only to primitive compile-time constants or to arrays of primitive compile-time constants.

// primitive data type
static final byte a = 1;
// primitive array type
static byte b[] = {1, 2, 3};

The Java Card system supports two optimizations for handling static fields. In the first, final static fields of primitive data types are inlined in the bytecodes of a CAP file. That is, the converter replaces the bytecodes that reference final static fields with bytecodes that load constants. This optimization saves data space taken up by primitive final static fields. However, because constants are inlined in bytecodes, this optimization may or may not save code space. Compared to referencing a static field, loading a byte constant is cheaper, loading a short constant (2 bytes) has no impact on code size, and loading an int constant (4 bytes) is actually more expensive. But most constants used in a Java Card system can be represented in a single byte. Thus, overall, inlining constants probably will save significant code space. For example, the byte type constants in the interface ISO7816 need not be stored, and numerous references to them from applets and the JCRE are replaced by less expensive bytecode.

In the second optimization, the converter preprocesses static field initialization. It creates in the CAP file binary images of static fields with their initialized values (an exception is static final primitive fields, which are inlined in bytecodes). Later, when the CAP file is installed, these static images are loaded onto the card. This converter optimization saves at least 7 bytes for each element initialized in a static array and at least 3 bytes for each static primitive field initialized. Many applets have predefined parameters (usually stored in arrays). For example, in a wallet applet, applet parameters could be the applet version number, currency code and exponent, security and authentication method descriptions, and so on. Declaring these parameter arrays as static or final static takes advantage of the converter optimization and can eliminate a significant number of array initialization bytecodes. However, if these parameters are not shared by all instances of the same applet—if instead the values of the parameters are customized for each applet and there will be multiple instances of the same applet coexisting on the card—they must be stored in instance arrays. In this case, an applet could declare static arrays to hold the common set of values while declaring instance arrays to store those parameters whose values are applet dependent.

Normally, an applet should not initialize static fields inside an instance method unless the initialization depends on the logic implemented in the method. The reason is that the instance method may or may not be invoked at runtime. Therefore, the converter cannot forego optimization on such static field initialization.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.60.192