On synthesizing parametric specifications of dynamic memory utilization

8 downloads 0 Views 234KB Size Report
Grenoble, France, April 6—14 2002. LNCS 2304. [8] P. Cousot and N. Halbwachs. Automatic discovery of linear restraints among variables of a program. In.
On synthesizing parametric specifications of dynamic memory utilization ∗

Victor Braberman

Diego Garbervetsky

Sergio Yovine

School of Computer Sciences Universidad de Buenos Aires Buenos Aires, Argentina

School of Computer Sciences Universidad de Buenos Aires Buenos Aires, Argentina

Verimag Grenoble France

[email protected]

[email protected]

[email protected]

ABSTRACT We present a static analysis approach for estimating the amount of memory dynamically allocated by a piece of code. The technique is suitable for imperative languages with highlevel memory management mechanisms like Java and resorts to ideas that come from the community of parallel computing and the field of pointer escape analysis. Firstly, we propose a general procedure for synthesizing a parametric specification which over-estimates the amount of memory allocated by a program method in terms of its parameters. Then, we introduce a series of extensions to deal with the problem of estimating memory consumption in a scoped-memory management setting. The proposed techniques can be used to predict memory allocation, both during compilation and at runtime. Applications are manyfold, from improvements in memory management to the generation of parametric memory-allocation certificates. These specifications would enable application loaders and schedulers to make decisions based on the available memory resources and the memory-consumption estimates.

1.

INTRODUCTION

Current trends in the embedded and real-time software industry are leading towards the use of object-oriented programming languages such as Java. From the software engineering perspective, one of the most attractive issues in object-oriented design is the encapsulation of abstractions into objects that communicate through clearly defined interfaces. Because programmer-controlled memory management inhibits modularity, object-oriented languages, like Java, provide built-in garbage collection [20], that is, the automatic reclamation of heap-allocated storage after its last use by a program. ∗

Partially supported by projects DYNAMO (Min. Research, France), MADEJA (Rhˆ one-Alpes, France), ANCyT grant PICT 11738 and IBM Eclipse Innovation Grants.

Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. To copy otherwise, to republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Copyright 200X ACM X-XXXXX-XX-X/XX/XX ...$5.00.

However, automatic memory management is not used in real-time embedded systems. The main reason for this is that the execution time of software with dynamic memory reclaiming is extremely difficult to predict. Therefore, in current industrial practices the use of garbage collection is simply forbidden. The typical approach is the following: the program allocates all the memory during the initialization phase and frees it at termination. This leads to a very inefficient use of the memory, usually resulting in over-dimensioning the physical memory at an unnecessary additional cost. Clearly, automatic memory management techniques that meet real-time requirements would have a tremendous impact on the design, implementation, and analysis of embedded software. These techniques would rule out programming errors produced by hazardous memory allocation and reclaiming which are both hard to find and to correct. This would drastically reduce implementation and validation costs while considerably improving software quality. To overcome the drawbacks of current garbage collection algorithms, the Real-Time Specification for Java (RTSJ) [2] proposes to use an application-level memory management based on the concept of “scoped memory”, for which an appropriate API is specified. Scoped-memory management is based on the idea of allocating objects in regions which are associated with the lifetime of a computation unit. Regions are deallocated when the corresponding computations units finish their execution [16, 30]. However, the RTSJ approach has two important inconveniences. First, determining the scope of objects is left out to the programmer. Second, the API requires to fix the size of the memory region associated with a scope when it is created. Several techniques have been proposed to address the first problem by automatically verifying the scoping-rules using pointer and escape analyses [28, 1] or discovering sets of objects which can be allocated using scoped-memory management [9, 15]. To our knowledge, nothing has been done so far to tackle the second problem which requires determining in advance the amount of memory to be allocated for all the objects in the scope. In particular, the problem of finding a finite upper bound on the memory consumption of a program, is in general, undecidable, even when the input are known, because it can be reduced to the halting problem [17]. This paper reports the first steps towards tool support for automatic overestimation of memory utilization. We present techniques based on static analysis that computes a symbolic expression of the memory consumption.

Firstly, we present a technique to analyze dynamic allocations done by a method. Given a method m with parameters p1 , . . . , pk we compute an expression over these parameters which over-approximates the number of objects created during its execution and the memory space these objects occupy. Secondly, we focus on programs with scoped-memory management. It can also be used to predict heap usage, assuming that garbage collector behavior is known a priori 1 . Our approach relies on the ability to analyze the relationships between objects and methods. Using pointer and escape analysis it is possible to conservatively determine if an object “escapes” or is “captured by” a method. Intuitively, an object escapes a method when its lifetime is longer than the method’s lifetime, so it can not be collected when the method finishes its execution. In contrast, an object is captured by the method when it can be safely collected at the end of its execution. Under this new framework, given a method m with parameters p1 , . . . , pk , and assuming that regions allocate the objects captured by the methods, we develop two algorithms that compute expressions over the parameters which overapproximate, respectively, the number of objects that escape and are captured by m, together with the memory space they occupy. The escape estimator can be used, before calling the method, to know the amount of memory that will be allocated in the active scope (the caller region or their parents regions in the call chain). The capture information can be used to estimate the size of a region before its creation. Moreover, knowing in advance, the total amount, the number and kind of objects can eliminate or drastically reduce memory fragmentation. Thirdly, we provide an algorithm such that, given a method m with parameters p1 , . . . , pk , computes an expression over these parameters which over-approximates the amount of memory required to run m. This estimator considers the eventual creation and collection of objects (under a scope memory management or similar) that may occur during the method execution and gives a conservative idea of the amount of memory required to run a piece of code. These techniques can be used to predict memory allocation, both during compilation and at runtime. Applications are manyfold, from improvements in memory management to the generation of parametric memory-allocation certificates. These specifications would enable application loaders and schedulers (e.g., [21]) to make decisions based on the available memory resources and the memory-consumption estimates.

Document Structure In Section 2 we present a motivating example and introduce some definitions and notations that will be useful for formalize our approach. We also introduce some already developed techniques we use when defining our method. In Section 3, we explain our general method for calculating the memory consumption. In Section 4 we specialize our method for scoped-based memory management, combining the algorithm with some results that come from pointer and escape analysis. In Section 5 we discuss how to improve our estimation when certain properties cannot be captured by the techniques previously described. In Section 6 we explore how 1

In fact, assuming a region management based on escape analysis [15], the garbage collector can collect objects at the end of the computation unit that captured them.

0: 1: 2:

0: 1: 2: 3: 4: 5: 6: 0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 0: 1:

void m0(int mc) { Object[] a = m1(mc); Object[] e = m2(2*mc); } Object[] m1(int k) { int i; Object b[] = newA Object[k]; for(i=1;i represents the call relation. (c, l, m) ∈ E means that the method c, at location l, calls method m. We assume that we can determine at compile time, for each call, exactly which method will be invoked, not being able to have more than one possible invocable method. Supporting inheritance and late binding is outside the scope of this work. Since currently we do not deal with recursive programs, a finite Call Tree can be obtained by unfolding the call graph. This unfolding is done by cloning and renaming the nodes that have more than one parent.

Figure 2: Call Graph and Call Tree of the proposed example A program control flow graph is a directed graph G =< N, E, entry, exit > where N represents the set of nodes in the graph and E the set of edges. entry and exit are specials nodes indicating unique start and ending points. Given a method m, Gm is the control flow graph of m which includes transitively the control flow graph of every method that m calls. Each node n ∈ N corresponds to one statement and has a label l ∈ Label to identify it in the original program. Since a called method is macro-expanded in the control flow graph each time it is invoked, labels are composed by the corresponding method name in method m call tree and its relative location. We call Creation Site every place (defined by its Label ) of the program where an object is created (i.e. there is a new or a newA statement). For simplicity we assume that new statements only create object instances. Constructors are assumed to be called separately. Calls to constructors are handled as any other method call.

shows the creation sites for each method of our example. CSm0 = {m1.2, m1.5, m2.3, m2.6, m2.7, m2.8, m2p.3, m2p.6, m2p.7, m2p.8} CSm1 = {m1.2, m1.5, m2.3, m2.6, m2.7, m2.8} CSm2 = {m2.3, m2.6, m2.7, m2.8} Table 1: Creation Sites of the example

2.3

Pointer and escape analysis

In order to infer scope information we use pointer and escape analysis [28, 1]. This is a static analysis technique that discovers the relation between objects. It has been used in several applications such as synchronization removal, elimination of runtime checks, stack and scoped allocation, etc. Here, we are interested in conservatively determining if an object “escapes” or is “captured by” a method. An object escapes a method when its lifetime is longer than that of the method. Let escape : M ethod → IP (CreationSite) be the function that returns the creation sites that escape a method. An object is captured by the method when it can be safely collected at the end of the method’s execution. Let capture : M ethod → IP (CreationSite) be the function that returns the creation sites that are captured by a method. In Table 2 we show the creation sites that escape and are captured by each method in our example. Figure 3 show a simplified representation of the result of escape analysis for some selected creation sites. escape(m0) = { } escape(m1) = { m1.2, m2.3, m2.6, m2.7 } escape(m2) = { m2.3, m2.6, m2.7 } capture(m0) = { m1.2, m2.3, m2.6, m2.7, mp2.3, m2p.6, m2p.7 } capture(m1) = { m1.5 } capture(m2) = { m2.8 } Table 2: Escaping and captured creation sites

CreationSites = { l ∈ Label | stm(l) ∈ {new Type, newA Type[. . . ]} } CSm denotes the set of creation sites reachable from the entry point of the method m control flow graph. Table 1

Figure 3: Simplified escape analysis graph for creation sites m1.2, m2.3, m2.6, m2.8, m2p.3, m2p.6 and m2p.8

2.4

Symbolic analysis

An invariant for a program location is an assertion over the program variables that holds whenever this location is reached in every program run. For our work we consider an invariant I over the set of variables V as a set of linear or non-linear constraints over V . Given a method m and a program label l, Ilm denotes an invariant predicate over the program variables, for the control flow graph of the method m, at the node labelled l. There is a lot of research on symbolic analysis techniques can be used to compute invariants. For example, [8, 7] propose an approach based on abstract interpretation [6], where the invariants are convex polyhedrons; [25] proposes the use non-linear constraint solving for obtaining linear invariants; [13] proposes methods based on symbolic evaluation that can handle some non-linear constraints. For java, languages for describing assertions are becoming popular (e.g. JML [22]) and several method for synthesizing invariants were proposed (for instance, [26],[11] and [14]). In this paper, invariants were generated “a la” Halbwachs [8]. The left column of Table 3 shows the invariants for each creation site reachable from method m0. cs

m0 Ics

m0 C(Ics , Pm0 )

m1.2 {k = mc}

1

m1.5 {k = mc}

1

m2.3 {k = mc, 1 ≤ i ≤ k, n = i}

mc

m2.6 {k = mc, 1 ≤ i ≤ k, n = i, 1 ≤ j ≤ n, j mod 3 = 0} m2.7 {k = mc, 1 ≤ i ≤ k, n = i, 1 ≤ j ≤ n, j mod 3 > 0} m2.8 {k = mc, 1 ≤ i ≤ k, n = i, 1 ≤ j ≤ n} m2p.3 {n = 2mc} m2p.6 {n = 2mc, 1 ≤ n, j mod 3 = 0} m2p.7 {n = 2mc, 1 ≤ n, j mod 3 > 0}

1 6 1 3 1 2

1 1 mc2 − mc+per(mc, [0, 0, − ]) 6 3 2 1 mc2 + mc + per(mc, [0, 0, ]) 3 3 mc2 +

1 2

mc

1 j j

m2p.8 {n = 2mc, 1 ≤ j ≤ n}

≤ ≤

4 3

1 2 mc + per(mc, [0, − , − ]) 3 3

4

2 1 mc + per(mc, [0, , ]) 3 3 3 2mc

per(x, list) = list[x mod size(list)]

Table 3: Calculated invariants (using Halbwachs technique) and Ehrhart polynomials (using Clauss technique) that count the analyzed creation sites for m0 Given set of constraints I, over a set of variables V = W ] P , C(I, P ) denotes a symbolic expression over P that provides the number of integer solutions for the remaining variables W . More precisely, C yields an expression which is equivalent to λ~ p.( #{ w ~ | I [W/w, ~ P/~ p ] } ). In general the resulting expression has the form: X γ(Condi (P )) ∗ Ei (P ) 1≤i≤k

where γ is such that γ(Condi (P )) = 1 if Condi (P ) = TRUE, 0 otherwise.

There are several techniques that obtain these symbolic expressions [4, 12, 27]. In [4], Ei s are Ehrhart polynomials 2 [10] over the given parameter variables. In [12], Ei s are integer-valued symbolic expressions that may consist of arbitrary divisions, multiplications, subtractions, additions, exponentiations, and maximum and minimum. Its operands can be parameters, integer constants and infinity symbols (∞, −∞). The right column of Table 3 shows the Ehrhart polynomials that express the number of instances for each creation site reachable from method m0 in terms of its parameters.

3.

PARAMETRIC CHARACTERIZATION OF THE AMOUNT OF MEMORY DYNAMICALLY ALLOCATED BY A METHOD

In this section we present an algorithm to compute an expression that over-approximates the amount of memory allocated by a method. Basically, the algorithm seeks for the creation sites reachable from a method, computes the corresponding invariants, counts the number of solutions as a polynomial in terms of the parameters, and adds them up. Firstly, we present the function computeAlloc that computes an expression in terms of method parameters that over-approximates the amount of memory allocated by a method and a selected set of creations sites. computeAlloc(m, CS) // returns an Expression (over Pm ) // requires CS ⊆ CSm res:=0; for each cs ∈ CS do m get Ics ; m alloc=S(Ics , Pm , cs); res:=res + alloc; end for; return res; Then, given a method m, the symbolic estimator of the memory dynamically allocated by m is defined as follows: memAlloc(m) = computeAlloc(CSm ) m The invariant Ics can be obtained using the techniques described previously in 2.4. It is described in terms of the program variables, constants and method’s parameters. If the creation is involved in one or several loops, the induction variables (i.e., those which have different values at different loop iterations) will appear in the invariant. The induction variables will belong to the set of free variables which are used to count the number of solutions. These variables have a direct impact on the next phase because they influence the number of integer solutions for the obtained invariant. After getting the invariant, the function S (see below) generates the memory allocation expression, using the method to count the number of times the creation site is invoked as a function of the methods parameters. For new statements, this is done by applying directly the symbolic analysis techniques described in the previous section. Nevertheless, in the case of the creation of arrays (i.e., newA), these techniques 2 A polynomial whose coefficients may vary depending on the parameters’ values.

need to be adapted considering the fact that an array is a collection of elements of the same type. To illustrate the problem, let us consider the creation site at label m2.3 (see Figure 1): Object[] f = newA Object[n]. The newA is executed each time the method m2 is invoked. When invoked from method m1, it will be executed k times. At each call the value of n will be equal to the iteration index i of m1. Then, the invariant of m2.3 is: m1 Im2.3 = {1 ≤ i ≤ k, n = i}. m1 The counting method yields the expression: C(Im2.3 , n) = k, leading to a wrong estimate of the allocated memory of size(Object).k. In fact, the correct amount of memory allocated is: X 1 n = size(Object). .k.(k + 1). size(Object). 2 1≤n≤k

To solve this problem we modify the invariant used to count the number of instances. To do so, we use the fact that X X X 1. n= 1≤n≤k

m0 S(Ics , Pm0 )

cs m2.6

m2.7 m2.8

1 1 1 1 5 size(Int) · ( mc3 + mc2 + per(mc, [− , − , − ])mc + 9 2 6 6 6 4 11 per(mc, [0, − , − ])) 9 9 1 2 1 size(Int) · ( mc2 + mc + per(mc, [0, 0, ]) 3 3 3 size(Int) · (2mc2 + 2mc)

4 2 2 ( mc2 + per(mc, [2, − , ])mc 3 3 3 2 2 per(mc, [0, − , − ])) 3 3 2 1 4 m2p.7 size(Int) · ( mc + per(mc, [0, , ]) 3 3 3 ·

m2p.6 size(Int)

+

m2p.8 size(Int) · 8mc 1

11

5

size(Int) ·

=

9

mc3 +

25

mc2 + (12 + 4 11  per(mc, [ , − , − ]))mc + 9 + per(mc, [0, − , − ]) + 6 6 6 9 9 1  7 2 size(Object) · mc + mc 2 2 memAlloc(m0)

6

1

Table 4: Symbolic expressions for memory consumption for some of the analyzed creation sites of m0

1≤n≤k 1≤h≤n

This allows us to add the constraint {1 ≤ h ≤ n} to the loop invariant. The new invariant will then be { 1 ≤ i ≤ k, n = i, 1 ≤ h ≤ n}, and the counting method will then yield the correct expression. According to the previous explanation, we define the function S as follows: S(I,pl,l) // returns an Expression over pl if stm(l)= new T res:=size(T)·C(I, pl); else if stm(l) = newA(T,e1 , . . . , en ) res:=size(T); for each i ∈ [1..n] do Invei :=I ∪ {1 ≤ hi ≤ ei } ; res:=res·C( Invei , pl); end for; end if; return res; where size(T) is a symbolic expression that denotes the size of an object of type T , C is the symbolic expression that counts the number of integer solutions for an invariant as defined in 2.4. Recall that the calculated invariant may be conservative (weaker than the ideal one) and, thus, the expression that gives number of integer solutions could bring an over-estimation of the number of iterations. In Table 4 we show the polynomials that over-approximate the memory allocated for (some selected) creation sites reachable from method m0 and the expression memAlloc(m0). Notice that, when the creation site is not an array, the polynomials for counting instances and measuring allocated memory differ with the ones on Table 3 only by the size factor. In the case of arrays, the latter may have a bigger degree (e.g., the creation site m2.6) as a consequence of the modification of the invariant explained above. The final expression is obtained by summing up all the symbolic expressions.

4.

APPLICATION TO SCOPED-MEMORY MANAGEMENT

The scoped-memory management is based on the idea of grouping sets of objects into regions that are associated with the lifetime of a computation unit. Thus, the objects are collected together when their corresponding computation unit finishes its execution. For our work we will assume that the program uses a scoped-memory management based on the escape and capture information that is obtained for each method. In particular, we assume that, at the method invocation, a new region is created and it will contain all the objects that are captured by this method. When it finishes, the region is collected with all its objects. An implementation of scoped memory following this approach can be found at [15]. The estimation can be also be used to estimate heap usage assuming a garbage collector whose behavior can be conservatively predicted by escape analysis.

4.1

Estimating the memory that escapes a method

In order to symbolically characterize the amount of memory that escapes a method, we use the algorithm developed in Section 3, but we restrict the search to creation sites that escape the method. memEscapes(m) = computeAlloc(escape(m)) In Table 5 we show the invariants and the memory-consumption expressions for the escaping creation sites of m1 and m2. Observe that expressions are defined only on the method’s parameters. This information can be used to know how much memory the method leaves allocated in the active regions after its own region is deallocated or to measure the amount of memory that cannot be collected by a garbage collector after method finalization.

4.2

Estimating the memory captured by a method

cs m1.2

m1 Ics

m1 S(Ics , Pm1 )

{}

size(Object) · k

1 1 size(Object) · ( k2 + k) 2 2 1 1 1 1 5 4 11 m2.6 {1 ≤ i ≤ k, n = i, 1 ≤ j ≤ n, j mod 3 = 0} size(Int) · ( .k3 + k2 + per(k, [− , − , − ])k + per(k, [0, − , − ])) 9 2 6 6 6 9 9 1 2 1 m2.7 {1 ≤ i ≤ k, n = i, 1 ≤ j ≤ n, j mod 3 > 0} size(Int) · ( k2 + k + per(k, [0, 0, ]) 3 3 3 1  1 5 2 1 1 5 3  memEscapes(m1)=size(Int) · k3 + k2 + ( + per(k, [− , − , − ]))k + size(Object) · k2 + k 9 6 3 6 6 6 2 2 m2.3

cs m2.3

{1 ≤ i ≤ k, n = i}

m2 Ics

m2 S(Ics , Pm2 )

{}

size(Object) · n

1 4 2 4 size(Int) · ( n2 + per(n, [ , , 0])n + per(n, [0, −1, − ])) 3 3 3 3 2 1 2 m2.7 {1 ≤ j ≤ n, j mod 3 > 0} size(Int) · ( n + per(n, [0, , ]) 3 3 3  1 2 4 2 n2 + ( + per(n, [ , , 0]))n + size(Object) · n memEscapes(m2)=size(Int) · 3 3 3 3 m2.6

{1 ≤ j ≤ n, j mod 3 = 0}

Table 5: Invariants and polynomials of the escaping creation sites of m1 and m2 To compute the expression over-estimating the amount of allocated memory that is captured by a method, we use the algorithm developed in Section 3, but we restrict the search to creation sites that are captured by the method. memCaptured(m) = computeAlloc(capture(m)) The resulting expression is a symbolic estimator of the size of the memory region associated to the method’s scope. In our example, the expression for the amount of memory captured by method m0 is the sum of the expressions shown in Table 4 for all creation sites except for m1.5, m2.8 and m2p.8 which are not captured by m0. Table 6 shows the expressions for the amount of memory captured by each method of our example. m

m1

memCaptured(m) 1 13 2 mc3 + mc + (2 + size(Int) · 9 6 11 5 1 4 11  per(mc, [ , − , − ]))mc+per(mc, [0, − , − ]) + 6 6 6 9 9 1 7  2 size(Object) · mc + mc 2 2 size(Int) · 9

m2

size(Int) · 4n

m0

Table 6: Symbolic expressions of the memory captured by methods m0, m1 and m2 This information can be used to specify the size of the memory region that be allocated at run-time, as required, for instance, by the RTSJ [2], and to mitigate fragmentation by preinforming memory managers about size and type information.

4.3

Estimating the memory required to run a method

Knowing the amount of memory captured by a method is not enough to easily deduce the amount of memory actually required to run it. Indeed, we must consider the sizes of

the memory regions of all methods it invokes (directly or indirectly), that is, all methods in its call tree. As an example, consider method m0. At location m0.1, m0 calls m1 which calls m2. At location m0.2, m0 calls m2 (m2p in the call tree). Under a scoped memory management, there will be three active regions for the call chain m0 → m1 → m2, and two active regions for the chain m0 → m2p. These two chains are independent as they are not simultaneously active. Let fm0 (m) be the expression that over-estimates (in terms of m0 parameters) the maximum size of the memory region used by m whenever m0 precedes m in the call stack (i.e., there is a path from m0 to m in the call tree). Thus, the maximum amount of dynamic memory necessary to run m0 is given by the following expressions:   peakm0 (m0) =fm0 (m0) + max peakm0 (m1), peakm0 (m2p)   peakm0 (m1) =fm0 (m1) + max peakm0 (m2)

peakm0 (m2) =fm0 (m2) peakm0 (m2p)=fm0 (m2p)

These expressions can be reduced to:   peakm0 (m0) = fm0 (m0)+max fm0 (m1)+fm0 (m2), fm0 (m2p) .

In general, the peak function is defined as follows:

peakmr (m) = fmr (m) +

max

(m,l,mi ) ∈ edges(CT (mr ))

peakmr (mi )

where CT (mr ) is the call tree of method mr and edges is the set of its edges. Assuming that for each method there is a region containing all objects captured by it, fmr (m) must accumulate the amount of memory captured by method m. This expression must be given in terms of the parameters of the analyzed method, mr . Since m can be invoked several times during the execution of mr , fmr (m) must yield the size of the largest region created by any call to m. In the example, method m0 calls method m1 which calls k times method m2. At each invocation, the size of the region for m2 changes because the number of objects created

depends on parameter n. Then, the expression for fm0 (m2) has to be the maximum region size for method m2 among all the k possible ones. In order to obtain such expression, it is necessary to choose a valid value for n (satisfying the invariant for the control flow graph of m0 at method m2 entry point) which maximizes the expression that characterizes the amount of memory captured by method m2. Since the region for m2 is defined by the expression size(Int) · 4n (see Table 6), the largest region is produced when m2 is called with n = i = k = mc. Thus, fm0 (m2) = size(Int) · 4mc. In general, the function fmr (m) is defined as follows: fmr (m) =λp~m r  Maximize memCaptured(m) mr subject to Im.entry [Pmr /p~m r ] mr Note that Im.entry has the following sets of free variables: Pm (method m parameters), Pmr (method mr parameters) and W (other variables in the invariant) while memCaptured(m), the symbolic expression for the memory captured by m (actually, any m clone in the call tree), is only in terms of Pm . In the example,

fm0 (m2) = =λmc  max(size(Int) · 4n) s.t.{k = mc, 1 ≤ i ≤ k, n = i}[mc/mc] =λmc  max(size(Int) · 4n) s.t.{k = mc, 1 ≤ i ≤ k, n = i} =λmc  max(size(Int) · 4n) s.t.{1 ≤ n ≤ mc} =λmc  size(Int) · 4mc

To calculate fm0 (m0), no maximization is required. This is because memCaptured(m) (see Table 6) is expressed only in terms of method m0 parameters as well as the global invariant. According to this analysis, Table 7 shows the resulting expressions for fm0 (m). m

fm0 (m)

m0

size(Int)

m1

11 5 1 4 11  per(mc, [ , − , − ]))mc+per(mc, [0, − , − ]) + 6 6 6 9 9 1 7  size(Object) · mc2 + mc 2 2 size(Int) · 9

·

m2

size(Int) · 4mc

m2p

size(Int) · 8mc

1 9

mc3

+

13 2 mc 6

+

(2

+

Table 7: Expression for function f for the example

5.

IMPROVING TIGHTNESS IN MEMORY ESTIMATION

For our motivating example, the method obtains symbolic expressions that exactly reflect the actual number of instances of each creation site and the amount of memory allocated by them. This is because, in this case, all constrains are linear (or linearizable), and then any of proposed methods get invariants which captures all the relevant relationships between variables. However, depending on the program under analysis, the method to find invariants and count the number of solutions chosen, the function computeAlloc can over-estimate more that necessary. Consider the following example: 0: void test(int n,Object a[]) { 1: for(int i=1;i0 . 2. Sthen -Selse ≤ 0 if (i, p1 , . . . , pn ) ∈ Polyhedron≤0 . S 3. Polyhedron>0 Polyhedron≤0 = Rn . In this setting, the original program can be instrumented as described previously and obtaining the desired linearization: for(int i=1;i0 ) Bthen if ((i, p1 , . . . , pn ) ∈ Polyhedron≤0 ) Belse } The better the polyhedron approximation are, the less number of solutions are counted twice (and thus, a tighter approximation is obtained).

6. 6.1

ON SUPPORTING OTHER ITERATION PATTERNS Supporting Java Collections

In this section we generalize the method to treat other iteration patterns pervading applications programming in Java:iterator-driven cycles over collections.

1. As our methods deals with integer-valued inductive variables, each iterator should be equipped with a virtual counter. These counters are initialized at iterator creation and incremented when the corresponding iterator.next() is applied. Consequently, loop invariants involving iterators will include a constrain like {0 ≤ iterator < collection.size()} 2. Since data structures (collections in this discussion) may be passed as parameters and allocation estimators are polynomials over integer parameters, abstract data structures must be observed as integers4 . One of the most natural observer for collections is its size. However, many integer-valued views are allowed depending on how smartly a relevant semantical parameter of complexity can be deduced in each particular case (e.g., the size of the largest integer contained member of the collection, the size of the largest collection inside the collection, the number of objects satisfying a given property, etc.). Those symbolic expressions are introduced as integer valued variables and assigned the corresponding value when method is invoked5 . Then, part of the resulting polynomial would be expressed on those variables if we perform the counting procedure informing those variables as parameters. Figure 5 shows a (very simple) representation of an dynamic array using a linked list of fixed sized nodes. The memory allocated by the method test depends on the size of the collection passed as a parameter. The actual allocation take place in the method newBlock where a new block of memory is allocated only when the previous block is full. Our method yields the invariant: test InewBlock.1 = {0≤ it < c.size(), len = it, len mod 5 = 0, how = 5}

and the corresponding allocation expression in term of the collection size: test S(InewBlock.1 ) = c.size() + (per(c.size(), [0, 4, 3, 2, 1])

6.2

Handling more complex iterations spaces

It is interesting to note that the idea of iterating a collection could go beyond the previously stated situations and be generalized also to regard more general cycles as iterator driven (perhaps supported in a less automatic way). For 4 This is a common trick in diverse disciplines such as complexity theory and category partition testing 5 Actually, this is not strictly necessary since an invariant checker or invariant inference engine could be informed declaratively about the property ensured over those variables using already defined terms as the collection observer size() in JML.

public class ArrayDim { class NodeArr { NodeArr next; Object[] arr; NodeArr(NodeArr next,Object[] arr) { this.next=next; this.arr=arr; } } NodeArr list,last; int len; 0: public ArrayDim() { 1: NodeArr list=null,last=null; int len=0; } 0: public void add(Object o) { 1: Object[] block; 2: if(len % 5 == 0) block=newBlock(5); 3: else block=last.arr; 4: block[len % 5]=o; 5: len++; } 0: Object[] newBlock(int how) { 1: NodeArr block=new NodeArr(null,new Object[how]); 2: if(list==null) list=block; 3: if(last!=null) last.next=block; 4: last=block; 5: return block.arr; } 0: void test(Collection c) { 1: ArrayDim a=new ArrayDim(); 2: for(Iterator it=c.iterator();it.hasNext();) 3: a.add(it.next()); } }

Figure 5: Collection Example instance, a typical fix point calculus could be understood as iterating a collection whose size is given by a virtual parameter fixedPointIterations which allow us to estimate the memory consumption depending on the number of iterations (which is a number generally unknown and non computable). If memory is a constrained resource, the polynomial may serve as a way to predict, before invocation, how many iterations can be safely supported. As another example, a typical traversal of the state space could be regarded as iterating a virtual collection that provides of the number of states yet to be visited and thus space complexity could would be expressed in terms of the size of the state space. More in general, the heuristic used in these cases is to create and iterate a collection consisting of the range of a variant function.

7.

RELATED WORK

Dynamic memory estimation has also been studied in [31] using a general and interesting approach based on symbolic evaluation. The analysis is not parametric in our sense, since they do not obtain a closed expression. Instead, given a function, the method consists in constructing a new function that symbolically mimics the memory allocations of the original one. The new abstract function (space bound function) is then executed over sets of parameters with unknown parts. It yields an overestimation of the bound, for the memory allocated by the original one, over the set of represented inputs. As stated in [31], this bound function might not terminate, even when the original program does, if the recursive structure of the original program depends on unknown parts of a partially know input structure (e.g., if recursion is driven by a parameter list, its size cannot be considered as an unknown). The cost of evaluation of the bound function being difficult to predict and potentially high, make this approach more suitable for off-line analysis such as profiling or

testing. In contrast, our method always terminates, even if the program does not(in the worst case it will inform that no polynomial bound was found). Unknown values do not cause any extra trouble. Our technique generates a polynomial whose evaluation cost is negligible compared to the cost of executing the corresponding method6 . Another technique has been presented in [17] which symbolically manipulates memory-consumption expressions, leaving unknowns that are not necessarily linked to method parameters. It supports polymorphism and it is inter-procedural. Although it is difficult to analyze this work due to the lack of examples, it seems not to be suitable for programs with dynamically created arrays whose size depends on loop variables. In such cases, it yields an over approximation similar to multiplying the maximum array size by the number of loop iterations. In contrast, our approach produces more accurate estimates for dynamic memory creation inside loops. Closer to our spirit, the work of Hofmann and Jost [19] statically infers, by typing derivation and linear programming, easy-to-evaluate expressions that depend on function parameters to predict memory consumption. However, their work differs from ours in many aspects. The technique is stated for functional programs running under a special memory mechanism (free list of cells and explicit deallocation in pattern matching). The obtained bound are linear bounds expression involving the sizes of various part of data. In particular, the size of the freelist required to evaluate the function is an expression on the input, while the freelist left is an expression on the result. On the other hand, our approach is meant for imperative languages with high level memory management and relies on pointer and escape analysis to infer objects lifetimes and it does not require explicit declaration of deallocations. The obtained bounds are polynomials in terms of methods parameters instead of linear expressions. Our approach combines techniques appearing in the field of parallelizing and optimizing compilers. They are traditionally applied to works on performance analysis, cache analysis, data locality, and WCET [12, 24, 5, 23]. As we know, the combination of such techniques to obtain specifications that predict dynamic memory utilization is novel.

8.

CONCLUSIONS AND FUTURE WORK

We have developed a technique to find symbolic estimators of dynamic memory utilization. We first presented an algorithm for computing the estimator for a single method. We then specialized it for scope-based memory management. Our approach combines techniques appearing in the field of parallelizing and optimizing compilers. It resorts to well studied techniques for finding invariants and counting integer solutions to sets of constraints. We believe that the combination of such techniques, and in particular, their application to obtain specifications that predict dynamic memory utilization is interesting and novel. Besides, it may be suitable for accurately analyze memory utilization in the context of loop-intensive programs. The estimators can be used both at compile time and run-time, for example, to set up the appropriate parameters required by the RTSJ scoped-memory API, to over estimate 6

They peak function may be an exception, but we are currently working in symbolically solving (off-line) the maximization problem

heap usage, to improve memory management and to accurately determine whether a new programm can be safely dynamically loaded and scheduled without disturbing the others programs behavior. We are integrating all the techniques in order to obtain a tool that will allow us to test the approach under more complex examples. Currently, escape analysis, program instrumentation and counting is done automatically but integration is manual. Our technique still does not support polymorphism (it is supported if it is completely solved at compile time), late binding and recursion. It is also interesting working in a more inter-procedural approach. Other aspect to explore is the optimization of our method. Slicing techniques [29] could help in reducing the number of variables and statements considered when building the invariants. On the other hand, techniques like [18, 17] can be used to filter out creation sites in our analysis by statically discovering creation sites executed only once in a run. Finally, we believe that convex optimization techniques for non-linear problems (e.g., [3]) might be suitable for solving symbolically the maximization problem for peak consumption.

9.

REFERENCES

[1] B. Blanchet. Escape analysis for object-oriented languages: application to Java. In OOPSLA 99, volume 34, pages 20–34, 1999. [2] G. Bollella and J. Gosling. The Real-Time Specification for Java. Addison-Wesley Longman Publishing Co., Inc., 2000. [3] S. Boyd, L. Vandenberghe, and M. Grant. Efficient convex optimization for engineering design. In IFAC 94, Rio de Janeiro, Brazil, pages 14–23. [4] P. Clauss. Counting solutions to linear and nonlinear constraints through ehrhart polynomials: Applications to analyze and transform scientific programs. In International Conference on Supercomputing, pages 278–285, 1996. [5] P. Clauss. Handling memory cache policy with integer points counting. In European Conference on Parallel Processing, pages 285–293, 1997. [6] P. Cousot and R. Cousot. Abstract interpretation: A unified lattice model for static analysis of programs by construction of approximation of fixed points. In POPL 77, pages 238–252, 1977. [7] P. Cousot and R. Cousot. Modular static program analysis, invited paper. In CC 02, pages 159–178, Grenoble, France, April 6—14 2002. LNCS 2304. [8] P. Cousot and N. Halbwachs. Automatic discovery of linear restraints among variables of a program. In POPL 78, pages 84–97, Tucson, Arizona, 1978. [9] M. Deters and R. K. Cytron. Automated discovery of scoped memory regions for real-time java. In ISMM 02, pages 25–35, 2002. [10] E. Ehrhart. Polynˆ omes arithmetiques et methode des polyedres en combinatorie. Series of Numerical Mathematics, 35:25–49, 1977. [11] M. D. Ernst, J. Cockrell, W. G. Griswold, and D. Notkin. Dynamically discovering likely program invariants to support program evolution. In ICSE99, pages 213–224, 1999.

[12] T. Fahringer. Efficient symbolic analysis for parallelizing compilers and performance estimators. The Journal of Supercomputing, 12(3), 1998. [13] T. Fahringer and B. Scholz. A unified symbolic evaluation framework for parallelizing compilers. IEEE Transactions on Parallel and Distributed Systems, 11(11), 2000. [14] C. Flanagan and K. Rustan M. Leino. Houdini, an annotation assistant for ESC/Java. LNCS, 2021:500+, 2001. [15] D. Garbervetsky, C. Nakhli, S. Yovine, and H. Zorgati. Program instrumentation and run-time analysis of scoped memory in java. Accepted in RV 04, ETAPS 2004, Barcelona, Spain, April 2004. [16] D. Gay and A. Aiken. Language support for regions. In PLDI 01, pages 70–80, 2001. [17] O. Gheorghioiu. Statically determining memory consumption of real-time java threads. MEng thesis, Massachusetts Institute of Technology, June 2002. [18] O. Gheorghioiu, A. Salcianu, and M. Rinard. Interprocedural compatibility analysis for static object preallocation. In POPL 03, January 2003. [19] M. Hofman and S. Jost. Static prediction of heap usage for first-order functional programs. In POPL 03, New Orleans, LA, January 2003. [20] R. Jones and R. Lins. Garbage collection. Algorithms for automatic dynamic memory management. John Wiley and Sons, 1996. [21] Ch. Kloukinas, Ch. Nakhli, and S. Yovine. A methodology and tool support for generating scheduled native code for real-time java applications. In EMSOFT’03, Philadelphia, USA, October 2003. [22] G.T. Leavens, K. Rustan M. Leino, E. Poll, C. Ruby, and B. Jacobs. JML: notations and tools supporting detailed design in Java. In OOPSLA 2000 Companion, Minneapolis, Minnesota, pages 105–106, 2000. [23] B. Lisper. Fully automatic, parametric worst-case execution time analysis. In WCET 03, 2003. [24] V. Loechner, B. Meister, and P. Clauss. Precise data locality optimization of nested loops. The Journal of Supercomputing, 21(1):37–76, 2002. [25] H. B. Sipma M. A. Colon, S. Sankaranarayanan. Linear invariant generation using non-linear constraint solving. In CAV 03, volume 2725, pages 420–433. LNCS, Springer Verlag, 2003. [26] J. W. Nimmer and M. D. Ernst. Static verification of dynamically detected program invariants:integrating Daikon and ESC/Java. In RV 2001,ENTCS, volume 55, 2001. [27] W. Pugh. Counting solutions to presburger formulas: How and why. In PLDI 94, pages 121–134, 1994. [28] A. Salcianu and M. Rinard. Pointer and escape analysis for multithreaded programs. In PPoPP 01, volume 36, pages 12–23, 2001. [29] F. Tip. A survey of program slicing techniques. Journal of programming languages, 3:121–189, 1995. [30] M. Tofte and J.P. Talpin. Region-based memory management. Information and Computation, 1997. [31] L. Unnikrishnan, S.D. Stoller, and Y.A. Liu. Optimized live heap bound analysis. In VMCAI 03, volume 2575 of LNCS, pages 70–85, January 2003.

Suggest Documents