Improve Real-Time Java performance and reliability with ScopedMemory Allocation: Part 2 - Embedded.com

Improve Real-Time Java performance and reliability with ScopedMemory Allocation: Part 2

Editor's Note: In Part 1 of thisarticle, the author states thecase for the use of scoped-memory rather than garbage collection in asystem of Java 5.0 style annotations which enables static analysistools. This part provides guidelines as to its proper use.

In order to enforce compliance with scoped-memory constraints atcompile time, we require that each component interface describe itsintentions with respect to scoped-memory usage conventions using Java 5meta-data annotations.

The following implementation of a Complex data type illustrates theuse of these annotations:

[1]     importjavax.realtime.util.sc.*;
[2] [3]     public classComplex {
[4]        public float real, imaginary; [5] [6]        public @ScopedThis Complex(float r, float i) { [7]            real =r; [8]            imaginary = i; [9]     } [10] [11]       public @CallerAllocatedResult @ScopedPure Complex add(Complex arg) { [12]           float r, i;
[13]           r = this.real + arg.real; [14]           i = this.imaginary +arg.imaginary;
[15]           return new Complex(r, i); [16]       }
[17] [18]        public @CallerAllocatedResult @ScopedPure Complex multiply(Complex arg){
[19]                float r, i; [20]                r = this.real * arg.real -this.imaginary * arg.imaginary;
[21]               i = this.real* arg.imaginary + this.imaginary * arg.real; [22]                return newComplex(r, i);
[23]        }
[24]     }

The @ScopedThis annotation on line 6 denotes that this constructormay be used to instantiate an object residing within a memory scope. Aspecial byte-code verifier enforces that the implementation of thisconstructor and all invocations of this constructor are entirelyconsistent with this annotation.

In particular, it assures that within the constructor, the implicitthis argument is never copied to any variable, such as a staticvariable or an outgoing argument that is not declared with a similar@Scoped annotation that would potentially survive longer than thisscope-instantiated object. The byte-code verifier also examines alluses of this constructor.

In general, it attempts to assure that all instantiated Complexobjects are allocated within scoped memory. It will only allow Complexobjects to be instantiated in immortal (permanent) memory if theprogrammer inserts special notations to override the default behavior.This is discussed below.

The @ScopedPure annotations on lines 11 and 18 signify that for eachof these methods, both the implicit this argument, and the explicit argargument might reside within scoped memory.

Thus, the byte-code verifier assures that within the body of thesemethods, the arguments are never copied into contexts that mightpersist beyond return from the respective method. Both of these methodsare also annotated with the @CallerAllocatedResult annotation.

This means that the caller will determine where the object is to beallocated. The typical usage is to allocate the return result withinthe body of the caller's context. This is demonstrated in the followingsample program:

[1]        public class doComplexComputation { [2] [3]            @ImmortalAllocation [4]            @ScopedPure [5]            @StaticAnalyzable [6]            public ComplexdoComputation(Complex arg1, Complex arg2) { [7]                  Complex a, b, i;
[8] [9]                   i = newComplex(0.0, 1.0);
[10]                a = newComplex(3.5, 0);
[11]                b =arg1.multiply(i);
[12]                b =arg2.add(arg1);
[13]                b =arg2.multiply(a);
[14]                return b; [15]            } [16]     }

In analyzing this program, the special byte-code verifier determinesthat the Complex objects instantiated at lines 9, 10, 11, and 12 mayeach be allocated within the local scope of this particular method.

However, this special byte-code verifier determines that the newobject allocated at line 13 will escape this local scope; thus itcannot be allocated in this method's private scope context. Since thismethod was not declared with the @CallerAllocatedResult annotation, thebyte-code verifier's only option is to allocate the returned object inimmortal memory.
Allocating immortal objects is generally considered anti-socialbehavior. If every program component were to allocate such objects,program reliability would suffer after the immortal pool becomesdepleted.

Under normal circumstances, the byte-code verifier would reject thisprogram component as illegal because it needs to allocate from immortalmemory. The only reason it allows the immortal allocation in thisparticular context is because the programmer authorized this allocationby declaring the enclosing method with the @ImmortalAllocationannotation.

To further reduce the likelihood of accidental or unexpectedallocation of permanent objects, the special byte-code verifierenforces that methods that are declared with the @ImmortalAllocationannotation may only be invoked from other methods that are declaredwith the same @ImmortalAllocation annotation.

In most real systems, the only time that it is appropriate toallocate permanent objects is during system initialization. In suchsystems, the main (startup) program will be declared with the@ImmortalAllocation annotation, as will the initialization subroutinesthat are invoked from the main program. But the components that areinvoked from the main program to perform the work of the applicationwill generally not be so annotated.

Additional annotations make it possible for programmers to createlinked data structures that reside within scoped memory. For example,if a class definition is annotated with the @ReentrantScope annotation,this means that all persistent objects allocated within the instancemethods of the class will be allocated from the same scope that holdsthe object itself.

This means it will be legal to create references from the originalobject to the newly allocated objects and from the newly allocatedobjects to the original objects.

In some cases, programmers may desire to implement algorithms ordata structures for which the compile-time analyzer is not able toprove that all assignments comply with the scoped-memory usageprotocols. The programmer may tell the byte-code verifier to allowthese assignments by declaring the corresponding method with the@AllowCheckedScopedLinks annotation.

Under this circumstance, the byte-code verifier will allowoperations that cannot be proven safe, and the code generator willperform a run-time check to assure that each such assignment is legal.

If any scoped-memory protocol errors are discovered at run time, theprogram component will throw IllegalAssignmentException, which is achecked exception. The byte-code verifier requires that any methoddeclared with the @AllowCheckedScopedLinks annotation also declareitself to throw IllegalAssignmentException.

Developers of safety-critical Java code are generally prohibitedfrom using the @AllowCheckedScopedLinks annotation, since this practicecannot be proven safe prior to execution.

To support scalable object-oriented composition of softwarecomponents, the byte-code verifier also enforces various consistencyconditions. For example, if a particular method declares that certainof its arguments are @Scoped, then all overriding methods must declare,at minimum, that the same arguments are @Scoped.

And if a method is declared with the @CallerAllocatedResultannotation, the byte-code verifier enforces that all overriding and alloverridden methods are also declared with this same annotation.

Determining the Size of Each MemoryScope
Reliable operation of real-time Java software depends on correctlysizing each memory scope within which the objects required forexecution are to be allocated.

A weakness of the traditional RTSJ specification is that there is nobuilt-in support to automatically and reliably determine scope sizes.This adds considerably to the developer's workload, and increases thelikelihood that the program will fail at run-time because scope sizeswere incorrectly calculated.

The disciplines recommended in this article provide three distinctmechanisms that programmers can use to specify scope sizes. Each methodthat allocates memory must use one of these three mechanisms todescribe its scoped memory size requirements.

Wherever practical, programmers are encouraged to use the@StaticAnalyzable annotation, as this mechanism automaticallyrecomputes the scope size each time the code is modified or ported to anew environment.

Use of the @StaticAnalyzableAnnotation
Hard real-time programmers can annotate certain program components withthe @StaticAnalyzable annotation to denote that they expect the hardreal-time Java static analyzer to automatically determine the CPU timeand memory resource requirements of this component.

To support the static analyzer, these programmers must restricttheir usage to a subset of full Java that can be efficiently analyzed.This annotation may itself be parameterized to differentiate betweensituations for which it must be possible to determine worst-case memoryneeds, and contexts for which it must be possible to determineworst-case CPU-time requirements.

Space is not adequate to allow a full description of the programmingconstraints that enable automatic analysis of static resourcerequirements. The key idea is that any method that is declared as@StaticAnalyzable may only call other methods that are also declared as@StaticAnalyzable.

All looping control structures within a @StaticAnalyzable contextmust be bounded in iterations by a specific programmer assertion, andany array or string allocations performed within a @StaticAnalyzablecontext must be bounded in length by special programming assertions.

The following code demonstrates the use of the @StaticAnalyzableannotation:

          @StaticAnalyzable          public void doSomething() {                AbsoluteTime time_1, time_2,time_3;
               UserClock clock;

               time_1 = new AbsoluteTime(30L,20);
               clock = new UserClock(time_1);

               // assumegetTime() is declared as @CallerAllocatedResult                time_2 = clock.getTime();                doSomethingElse();                time_3 = clock.getTime();                doSomeMore();        }

Use of @ScopedMemorySize annotation
For methods that are not static analyzable, this annotation allowsprogrammers to specify the desired size of the method's scoped memoryallocation context, measured in bytes. This annotation takes variousparameters to specify the desired scope size.

The total scope size is represented by the accumulation of sizerequests represented by the annotation's attributes. One possibleattribute simply identifies the number of bytes to reserve, representedas an integer constant.

Alternatively, the size can be represented symbolically, in terms ofa total number of instances of objects of various types. The examplecode below specifies that the scope must be large enough to representone instance of a UserClock and three instances of an AbsoluteTime:

            @ScopedMemorySize(instances = {1, 3},                                                            types = {UserClock.class, AbsoluteTime.class})            public void doSomething() {                    AbsoluteTimetime_1 = new AbsoluteTime(30L, 20);                    UserClockclock = new UerClock(time_1);
                   AbsoluteTimetime_2, time_3;

                  // assume getTime() is declared as@CallerAllocatedResult
                   time_2 =clock.getTime();
                  doSomethingElse();
                   time_3 =clock.getTime();
                   doSomeMore();         }

Use ofSizeEstimator.ensureScopeCapacity()
For methods that do not have the @ScopedMemorySize or @StaticAnalyzableannotation, programmers can determine the method's scope size byinvoking the static SizeEstimator.ensureScopeCapacity() method, passingas its SizeEstimator argument a reference to a SizeEstimator objectthat represents the requested scope size.

SizeEstimator is one of the classes defined in the RTSJspecification. Once a SizeEstimator object has been instantiated, itserves as an accumulator to represent the cumulative memoryrequirements associated with each of the object's reserve()invocations.

When using this mechanism to specify the scope size, the programmeris required to arrange his code so that the only object allocatedwithin the scope prior to invocation of the ensureScopeCapacity()method is the single SizeEstimator object which will be passed as itsargument.

The following code demonstrates the use of the ensureScopeCapacity()method:

    public voiddoSomething() {
               SizeEstimator z = newSizeEstimator();
               AbsoluteTime time_1, time_2,time_3;
               UserClock clock;

               z.reserve(UserClock.class, 1);
               z.reserve(AbsoluteTime.class, 3);
               SizeEstimator.ensureScopeCapacity(z);

               time_1 = new AbsoluteTime(30L,20);
               clock = new UserClock(time_1);

               // assumegetTime() is declared as @CallerAllocatedResult                time_2 = clock.getTime();                doSomethingElse();                time_3 = clock.getTime();                doSomeMore();     }

Evaluation
Real-time programmers who adhere to the programming disciplinesdescribed in this article are assured that their code will run morereliably because byte-code verification proves that various potentialviolations of scoped-memory protocol errors will not occur.

Furthermore, the system can automatically calculate the sizes of allmemory allocation scopes to eliminate the possibility that a componentwill fail to fulfill its assigned responsibilities because it cannotallocate the temporary objects needed for its computations.

Maintenance of real-time Java software is greatly benefited becauseeach component's scoped memory usage conventions are clearly identifiedin the annotated interface definition of the component.

Maintenance programmers who are required to make incremental changesto existing software components, or to implement new subclassdefinitions, or who find it necessary to combine independentlydeveloped components, have no question about the component's intendedinteractions with scoped memory protocols.

If the maintenance programmer accidentally violates the declaredscoped memory intentions of the component, the byte-code verifier willimmediately notify the programmer so that he or she can correct theerror. And if maintenance changes impact the required sizes of acomponent's memory scopes, the new scope sizes will be automaticallyrecomputed.

Performance of real-time Java code that complies with thesescoped-memory disciplines also benefits significantly. Because thebyte-code verifier assures that the programmer strictly adheres to allscoped-memory usage protocols, there is no need to check at run-timefor compliance with these protocols.

After eliminating these run-time checks, the code runs much faster.Experiments with a prototype implementation of this new draft standarddemonstrate that programs written according to these conventions runover three times faster than traditional RTSJ components on importantperformance-critical benchmarks.

To read Part 1, go to “Why an alternative togarbage collection is needed.”

Kelvin Nilsen, Ph.D., is chief technology officer at AonixNorth America

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.