A Framework-Based Environment for Object-Oriented Scientific Codes

Frameworks are reusable object-oriented designs for domain-specific programs. In our estimation, frameworks are the key to productivity and reuse. However, frameworks require increased support from the programming environment. A framework-based environment must include design aides and project browsers that can mediate between the user and the framework. A framework-based approach also places new requirements on conventional tools such as compilers. This article explores the impact of object-oriented frameworks upon a programming environment, in the context of object-oriented finite element and finite difference codes. The role of tools such as design aides and project browsers is discussed, and the impact of a framework-based approach upon compilers is examined. Examples are drawn from our prototype C++ based environment.

Reusable designs, implemented as object-oriented frameworks [14] [15] [18], are key to object-oriented scientific programming.A framework describes the basic elements used to create a general solution.But a framework is not just a collection of design guidelines or libraries; it is an integrated collection of components and interfaces that is designed to be easily extended into a working code.By choosing among various particular components, and by Frameworks OON-SKI '93, The Object-Oriented Numerics Conference, April 1993 tailoring components to the exact problem at hand, a user can adapt a framework to yield the desired computation.
Framework-based programming is a logical extension of object-oriented programming.Quite often, a developer needs only to tailor an existing component by adding new functionality, or by modifying existing behavior.Objectoriented languages support this activity by providing ways to incrementally layer new behavior onto existing components.Adding the new behavior derives a new kind of component while the existing components are not changed.
The end user of an object-oriented framework is principally involved in specializing existing class elements to provide new or refined behavior.Thus the dominant use of inheritance in a framework is to support specialization of classes.This usage leads to wide spread use of inheritance, "fat" interfaces [28] and dynamic function dispatches.
In providing design reusability, frameworks offer • a design structure for developing reusable components, • a unit of transportability among diverse computer architectures, and • a semantic structure for developing framework-cognizant tools.

Reusability of Components
Designing reusable components is difficult.First, a reusable component is necessarily general.When an already-developed module is to be made reusable, the designer's hardest task is to decide how the module should be generalized.Second, components are never used in isolation, but are combined with other components.Without considering the desired forms of interaction, a reusable component might not be, in fact, usable when combined with other components.A design framework provides the guidelines needed to solve these problems.Since the framework itself is general, the framework implicitly determines those ways in which a component must be general.Since the framework specifies the necessary interactions among its components, the framework explicitly determines the various interactions among components that must be supported.Adoption of a common set of frameworks also creates the opportunity to exchange components of codes, as in the object-component industry now arising in general programming.Since the components will work with the common framework, they will automatically work together.These features make it possible in the future to develop new codes rapidly, to modify existing codes readily, and to reuse codes in a matter of hours or days.
Transportability Because frameworks provide domainspecific abstractions, frameworks provide a natural structure for moving codes among architectures.A framework should be architecture independent, but may be re-implemented using different library components for different architectures.In effect, the framework stays the same while implementation details differ.
Four levels of transportability are evident in a framework based environment.Language translators provide the first level; by compiling and optimizing for different architectures, programs can occasionally be ported without change.Component definitions provide the second level of transportability: different implementations of matrices, for example, might be targeted for different architectures.The supporting code in the framework provides a third level.At this level, the choices made by a designer will still hold, but an alternative implementation of the framework itself may be useful.At the fourth level, the user may choose to revise prior implementation choices to derive a new program from the framework.For example the user might choose to move from an implicit to an explicit solution method using framework-cognizant tools to replay and revise an earlier design.

Framework-cognizant tools
Frameworks provide a meaningful and useful structure for developing support tools.Experience with language-based "intelligent" program editors, such as the Pan system [4], indicates that attempting to provide extensive support at the level of a programming language is probably inappropriate; even the forms of global information available to a user are limited to artifacts of programming [30].For example, a languagebased environment can often provide an answer to the question "What functions call function A?" Without design-specific knowledge, however, they cannot answer the question "Why is function A called?" Frameworks, with distinct operational components and definable interactions, provide a higher-level "pattern language" to which a useful design semantics can be applied.The connections among components can be annotated with semantic information, and the choices of made by a user can be traced and gathered into a design rational.It is at the framework or design level that intelligent tools be integrated.

Example: The Computation-Space-Quantum-Controller Framework
A framework provides a way to reuse designs: in our case methods for solving scientific problems [24] [25] [26].In this section, we provide a brief introduction to the Computation-Space-Quantum-Controller framework for finiteelement and finite-difference codes.
Figure 1 shows a high-level overview of the framework.
As shown in the figure, each numerical code consists of one or more Computations.Each Computation provides the data and the solution for a single problem.Within a computation is a Controller and a Computational Space.The Computational Space (or "Space") is where the actual computation takes place; the Controller is an object that starts (and continues) the computation by sending messages to the Space.The effect is to move the outer loop of a numeric computation into the Controller.Since Spaces contain the data being manipulated, they are responsible for managing their own input and output.
Within Computations, there are several interlocking and mutually dependent classes: LoopControl, Controller, ObjectHavingState, Space, and Quantum.Each of these is an abstract base class.Figure 2 illustrates the derivation hierarchy for these core classes.Multiple inheritance is used to enforce two restrictions: that a Space support the same control interface as a LoopControl, and that a Space and a Quantum share the properties common to ObjectHavingState.A Space can also contain objects derived from ObjectHavingState.Thus Spaces can contain embedded subspaces as well as Quanta.
Each of the core classes plays a single role in the code: a LoopControl provides the switches and dials for controlling iteration and checkpointing.ObjectHavingState is the basic container for data and scientific behavior.Class Quantum is primarily a computational element.The Space is an abstraction that provides both data and control.Finally, the Computation class provides the stage for the other players.
The ownership relationships (HAS-A) among the core classes are shown in Figure 3.Comparison Figures 2 and 3 show that a space both ISA and HAS ObjectHavingState.
The ability of a Space to contain objects that are also Spaces provides much of the flexibility in the framework.Spaces may organize their subcomponents by means of an abstract supporting class called a Neighborhood.Conceptually, the Space manipulates many instances of objects which are derived from ObjectHavingState, and many instances of Neighborhoods.In practice, a space may be able to minimize the actual number of instances by representing them implicitly, or by carefully managing a few prototypical instances.An example of the latter case is when a variant of a Neighborhood is used to represent an element in a finite element code; in many cases it is more efficient to swap Quanta representing nodes in and out of a single prototypical Element than to create one instance of an Element for each element appearing in the model.
Why the distinction between Space and Quantum?In our model, a Space glues a number of ObjectHavingState objects (typically Quanta) together.Within the Space, the Quanta may be represented explicitly as objects; implicitly by storing only their data fields; or as virtual Quanta in which the internal representation may not be visible, but the Quanta appear to be explicitly represented.There is only a tenuous connection between explicitly represented Quanta and explicit codes; the framework allows an explicit code to be written using either implicit or virtual Quanta, but the use of explicit Quanta generally indicates the use of an explicit solution method.

Supporting Frameworks in a Programming Environment
Effective use of design frameworks and object-oriented development requires an innovative development environment.The tools in the environment must understand and respond to the underlying framework and programming techniques, as well as the programming language itself.Such tools include design aides that assist a user to elaborate a framework into a working code, browsers that allow a user to manage libraries of components, and compilers that directly support the framework-based approach.
Figure 4 illustrates the overall architecture of a framework-based environment.As expected, the frameworks themselves play a central role.The essential tools include a design aide that mediates between users and the framework, a project browser that simplifies access to the codelevel aspects of an application, and a translator (or compiler) that is tuned for compiling the resulting programs.The data managed by the environment include the frame- works themselves, the libraries of components, the configured programs, and records of the choices made by a user in elaborating her programs.The latter is denoted by the "User's Notebooks" in Figure 3.The next sections discuss the role of the design aide and the translator in more detail.
While our experience to date is limited, we remain convinced that frameworks also provide a structure for building truly knowledgeable tools.Prior experience with language-based tools [4] [30] has shown that the programming-language level is too low to provide the kinds of assistance an end user can best use.Since a framework provides a problem-solving approach that is domain-specific, it provides a natural structure for supporting high level assistance.For example, while a framework may support arbitrary combination of its elements, a framework-cognizant design tool can provide many techniques for guiding the user's choice of components.

Design Aides and Project Browsers
A design aide is a sophisticated mechanism that helps the user elaborate a framework into a complete program.The design aide uses information accompanying the framework, object, and class modules to assist the user.For instance, a particular finite element code may make commitments on boundary conditions.The design aide can recognize this, using information about the methods and class specifications of the objects and classes the user has chosen.This will assist not only the sophisticated user in designing complex codes quickly, but also remind the novice of the important parameters of the code building process.In a simple sense the design aide can be seen as a novice tutor, assisting the beginning numerical program- mer in understanding the important aspects of the code he or she is developing.
The design aide also assists the user by maintaining notebooks of designs for scientific codes.By replaying a design from the notebook, a user can reconstruct (or modify) a previously implemented code.Finally, an intelligent design aide, one augmented with rule-based expertise, can help to guide a user in making her choices.
A screen dump of a prototype design aide for the Computation-Space-Quantum-Controller framework is shown in Figure 5.This prototype allows a user to select and modify components, either through the interface or through a combination of the design aide and direct browsing.In the illustration, our user has selected several components, indicated by the filled-in selection wells.At this point, the Quantum component has yet to be elaborated.
In Figure 6, the user has added instance variables and equations to the quantum.The illustration shows both the browser for equations and fields, and the inspector being used to add a heat transfer equation.In this case, the user has chosen to implement her equations directly in C++, augmented by higher level operations supported by the design aide.The notation "N.sum(temperature)" in the equation will be translated into a special C++ iterator function that sums the values of the "temperature" instance variable over all of the neighbors of the Quantum.
Project browsers help users to inspect the objects, classes, and inheritance relations in the environment.Unlike design aides, which are oriented toward non-programmers, project browsers allow programmers to view, extend, and modify frameworks, libraries, and codes at the source code level.Additionally, a project browser can exploit annotations present in frameworks to provide enhanced functionality.
A key feature of a project browser is its ability to provide integrated configuration management.By collecting information about the entire program into a program repository, the environment can better support both incremental compilation and interprocedural optimization.

Language and Translator
The underlying object-oriented programming language has several effects on the environment.First, it determines just what techniques can be used.For example, C++ supports multiple inheritance, making it easier to describe and implement the Computation-Space-Quantum-Controller framework presented above.Second, the language determines the degree of optimization that an environment can provide.Third, the size and complexity of the language has many implications for the ease with which an environment can be constructed, extended, and modified.
The presence of a translator that is fully integrated into an environment allows other tools to use translator facilities without replicating code.Reuse of translator components occurs at two levels: by embedding calls to the translator itself, and by linking directly to phases and other facets of the translator.For example, the design aide must avail itself of the analysis phases of the translator in order to better support the user.These phases, and their results, must be sharable within the environment.Similarly, the class and project browsers require information derivable from the source modules.

Language: C++
C++ is an evolving object-oriented language based on C [34].C++ differs from C by adding classes, inheritance, user-defined overloaded operators, dynamic function binding, reference variables, and run-time exceptions.Opti- Table 1 briefly enumerates some of the features of C++ and their impact upon compiling frameworks.In the table, a "+" denotes a feature helpful to an optimizer and a "-" denotes a feature detrimental to optimization.
Locality of reference assists an optimizing compiler, since locality makes it more likely for a compiler to be able to determine the effect of operations.Classes help the optimizer since they support fine-grained encapsulation.On the other hand, classes can complicate the work since classes tend to proliferate scopes.Similarly, member functions operate in encapsulated spaces, but tend to be numerous and small.While small functions are "good" programming practice, they present problems to optimizers which want larger sections of program text to work on.On advantage of procedure integration (or "inlining") is that it exposes a larger range of code for an optimizer to work on.Function calls also inhibit optimizations.
Templates have much the same effect as classes, but have the additional benefit that different template instantiations can be optimized in different ways.Consider a Matrix template that can be instantiated using integers or doubles as the elements in the matrix; the compiler will be able to  References and static members tend to help the optimizer by allowing a programmer to better control encapsulation and to better indicate the use of pointers.This contrasts well with C, in which pointers are used to implement both dynamic data structures and to effect call-by-reference.

Impact of C++ features on optimization
Exceptions cause two difficulties.First, they may incur either storage or execution time overhead when a try-block (the block that specifies a possible handler) is entered.
Second, since an exception may cause control to leave a block immediately after a function is called, the optimizer cannot assume that control will continue normally after a call.In particular, the optimizer cannot leave values in temporary memory (registers, in particular) unless it is prepared to restore those values when an exception is thrown.Fortunately, techniques exist to handle this problem.
Finally, overloaded operators and virtual function dispatch make good coding and optimization more difficult.Overloaded operators are problematic since they give the programmer the appearance of being primitive operations without giving the compiler sufficient information to manage their resources properly.Virtual function calls are problematic since they complicate the call graph and can hide possible optimization.Methods for dealing with both overloading and virtual calls are discussed in the next section.

Optimizing Object-Oriented Scientific Programs
A translator in a framework-based, object oriented, scientific code environment must provide a wide range of optimization techniques to assure that the final programs achieve necessary performance standards.The needed optimizations include both the usual intraprocedural and interprocedural techniques as well as new techniques specific to object-oriented programs.
Especially for framework-based scientific codes, interprocedural analysis is essential.To be completely effective, interprocedural optimization demands full knowledge of the entire program, not just a function, a file, or a class.On the surface, this is contrary to the notion of object-oriented programming as iterative enhancement, in which encapsulation is used to hide the bulk of the details of the program from the programmer.However, it is the translator that is violating encapsulation boundaries, not the programmer.Tools such as project browsers serve as useful intermediar-ies between the user and the translator by assisting the user in designating the actual configuration of the program and by transferring that information to the translator.The end result is that the user may not even be aware that she is providing the compiler with the information needed to complete interprocedural analysis.
Intraprocedural Optimization All of the usual intraprocedural optimizations [1], such as strength-reduction, constant folding, code motion, copy propagation, and common subexpression elimination are needed for object oriented codes.These techniques are not specific to object oriented codes, and are presented in most textbooks that cover optimization of imperative languages.
In C++, the ability to redefine operators encourages developers to create new concrete data types.A concrete data type appears to be a primitive type: it can be declared, assigned and passed as an argument just like a primitive type [13].For example, a C++ programmer is free to define and use infix expressions such as where A and B represent vectors, C is a matrix, and "+" and "*" have user-defined meanings.
As presently defined, C++ does not provide sufficient information about user-defined operators to the translator to effect even common code improvements.Without extralinguistic information, a translator may not be able to bring its full range of techniques to bear on the overloaded operators.
To a programmer, the appearance of the user-defined operators suggests that they will work "just like" the standard operators.These appearances are deceiving, but nowhere more than in the arena of resource management.It is wellknown that the management of temporary objects is a difficult problem in C++ class libraries [10] [17].
One method for assisting the translator is to provide annotations on class and method definitions.Annotations, in the form of compiler directives, are already used in many C and C++ translators.Properly expressed, annotations can provide control over optimization, can convey semantic information to the compiler, can be portable, and do not require language extension.
One approach to annotations uses algebraic equations together with cost estimates to express potential transformations.
While not yet fully implemented, our goal is to use the equations as rewrite rules in the same way that other rewrite rules are applied in advanced compilers [23].

Consider again the matrix expression
If the compiler can determine that A, B, and C do not share storage, this code can be rewritten as Suppose that both a *= operator and a += operator are defined.Given the rewrite rules , , and , an optimizer can easily rewrite this code to for a savings of at least 2 function calls in C++, and possibly saving the creation and destruction of two temporary values along with copying.Adding such rewrite rules requires two mechanisms in a compiler: the ability to attach annotations to symbols or other language elements, and the ability to perform rule-directed rewrites.The trick, then is to be able to detect when A, B, and C do not share storage.This requires some form of interprocedural alias analysis, along with information about storage management.
As an example, consider a Vector class having operations * and +, and consider the vector expression The simplest translation of this code from C++ into C results in several nested function calls: With interprocedural constant propagation, it may be possible to determine that and .In this case, the loops can be jammed: Straightforward transformation then yields a nicely vectorizable loop using vector chaining.Again, this depends upon the presence of adequate analysis: interprocedural constant propagation to allow the loop jamming 1 , alias analysis along with subscript analysis in the target compiler to detect that the loop is vectorizable.

Function Specialization
Framework-based programs make heavy use of virtual (dynamic) function dispatch.It is a virtue of object-oriented languages that any specific object A can be used wherever a more general object B can be used, so long as A is derived from B. This virtue, of course, has a cost in the form of virtual functions.Virtual functions have two costs: they inhibit optimization and they incur cycles during execution.
In a framework, most of the apparent classes are abstract; they will be replaced by specific classes and objects quite uniformly in the resulting code.For example, while a component may be defined in terms of a Matrix object, the final code might use only one specific class of matrices.In this case, the virtual function calls could be eliminated in the optimized code in favor of direct calls to the class being used.Function call specialization [12] [22] is the elimination of runtime procedure dispatch by determining at compile time the actual function being invoked.Specialization requires interprocedural type propagation, along with the ability to examine the entire program being compiled.
1. Full interprocedural constant propagation is not essential to this example; a compiler can generate multiple versions of the overall procedure by dynamically testing the sizes, and if they are all equal, executing the jammed and optimized loop.For example, consider a C++ fragment in which the doSomething member function is invoked on anObject. anObject.doSomething() The actual function invoked depends on the type of anObject: it may be inherited or it may be a member function defined in some class derived from anObject's class.In the presence of a virtual function, a basic translator will generate a dynamic procedure call that consults the table of functions associated with anObject's class during execution.
However, whenever the actual class of the recipient (anObject in this case) can be determined at compile time, the dynamic call can be replaced by a static call.Better, by determining the exact definition of doSomething, the translator can integrate the body of doSomething directly into the loop.Procedure integration may create new opportunities for improvement.Integrated configuration management also supports this optimization by providing access to all of the sources needed for procedure integration, whether or not a programmer has specified the functions to be "inline" using the nominal C++ directive.
Many C++ translators already generate direct calls to virtual functions provided that the type of the recipient can be derived locally.However, interprocedural type propagation will enable a compiler to fully deploy this optimization.In its simplest form, type propagation is just a form of constant propagation on type values.
Specialization does not require that only a single function be invoked.For example, consider a code fragment in which an iterator over a list of Shapes invokes member functions on each individual shape.
for (p = firstShape; p != 0; p = p->nextInList()) { p->resize(.The details of run time type determination are omitted.In this case, the tests isA... can be generated by the compiler.The proposed run-time type identification facility being considered by the C++ standards committee also sufficient [29].
Performing specialization effectively conflicts with the separate compilation model embraced by C and C++.Ideally, an optimizing compiler for C++ will have access to all of the source code for an application.Full access to the source code does not, however, compromise the objectoriented programming model.When full access is unavailable, the compiler must fall back to conservative assumptions and produce correct output.

Partial Evaluation
Partial evaluation results from combining a (possibly empty) subset of a program's data with a program to produce a new, simpler, program [9].In a sense, conventional optimizations such as constant folding are just forms of partial evaluation using an empty input data set.
Berlin [7] [8] has shown that partially evaluated scientific codes can show significant speedups.While it is unclear how to extrapolate these results from small Scheme programs to large scientific codes, the results are encouraging.Koo and Sundaresh [19] have recently shown that partial evaluation can be used to implement function call specialization.Their work is based on a high-level semantic model, but the results confirm the following observation: by using partial evaluation and by tracking the types of the objects created, a system can determine the actual types of objects involved in virtual function calls.Thus, rather than implementing a static analysis to propagate data types, the system simply performs an abstract interpretation on the program and tracks the results.Krishna [20] is currently working on the general problem of partial evaluation in C and C++ for scientific codes.
On its own, using partial evaluation would be too expensive to apply to most programs.However, an optimizer can use results from the partial evaluation to simplify other optimization passes such as interprocedural constant propagation.In the long term, partial evaluation may become an important technique in optimizing large scientific codes because many scientific codes operate on relatively fixed data sets for which a partially evaluated program would be appropriate.

Conclusion
This paper has presented a framework-based environment for object-oriented scientific programming and has examined the impact of a framework-based approach upon programming environments for object-oriented scientific codes.The use of a framework simplifies the creation of domain-specific, intelligent tools that can assist in the elaboration of programs from the framework.These same tools can be used to support project browsing and configuration management.With integrated configuration management, the environment is able to provide the interprocedural analysis needed to fully optimize scientific and numerical programs.Finally, the paper has briefly touched upon optimization of object-oriented codes, including function specialization, partial evaluation, and the need for interprocedural analyses.
Figure 2Derivation Hierarchy for Core Classes Figure 3Key ownership relations

Figure 5
Figure 5Using the design aide to specialize a Computation

TABLE 1 .
C++ FacilityImpact on OptimizationClasses± Proliferation of scopes Exceptions − Inhibits/complicates optimization Member functions ± Proliferation of functions Overloaded operators − Management of temporaries References + Well-behaved pointers Each iteration involves three dynamic function dispatches.Now suppose that a variable p can refer to any of several different classes derived from Shape.If the derived classes are known at compile time (or even if only a subset is known), a compiler can factor out the dynamic dispatch and then use statically compiled calls: