Paper Menu >>
Journal Menu >>
![]() J. Software Engineering & Applications, 2009, 2: 144-149 doi:10.4236/jsea.2009.23021 Published Online October 2009 (http://www.SciRP.org/journal/jsea) Copyright © 2009 SciRes JSEA Applying Heuristic Search for Distributed Software Performance Enhancement Omid BUSHEHRIAN Computer and IT Department, Shiraz University of Technology, Shiraz, Iran. Email: [email protected] Received April 2nd, 2009; revised April 28th, 2009; accepted May 4th, 2009. ABSTRACT Software reverse engineering and reengineering techniques are most often applied to reconstruct the software archi- tecture with respect to quality constra ints, or non-functional requirements su ch as m aintainab ility or reusability. In this paper, the performance improvement of distributed software is modeled as a search problem that is solved by heuristic search algorithms such as genetic search methods. To achieve this, firstly, all aspects of the distributed execution of a software is specified by an analytical performance evaluation function tha t not only evaluates the curren t deployment of the software from the performance perspective but also can be applied to propose the near-optimal object deploy- ment for that software. This analytical function is applied as the Heuristic search objective function. In this paper a novel statement reordering method is also presented which is used to generate the search objective function such that the best solution in the search space can be found. Keywords: Performance Engineering, Heuristic Search Methods, Software Reverse Engineering 1. Introduction Automatic software reverse engineering and reengineer- ing techniques are most often applied to reconstruct the software architecture with respect to quality constraints, or non-functional requirements such as maintainability or reusability [1–5]. However, there has been no effort to assess the architecture of existing distributed software from the performance viewpoint. All the architectural level performance engineering techniques are focused on the performance assessment at the early stages of the software development life cycle. However, the imple- mented software still may not meet its performance pro- visions and needs to be modified to improve the per- formance. In this paper, a novel software reengineering approach is presented that proposes some alterations to the de- ployment of the distributed software to improve its over- all performance. The performance improvement is achieved by providing the chance for concurrent execu- tion among method calls including the remote calls or local ones. The concurrency among the method calls is obtained when some of the blocking invocations are transformed into non-blocking or asynchronous form. However, this transformation entails execution overheads that should be considered very carefully. Therefore, the question is how to automatically find a set of invocations to be transformed to non-blocking such that the highest amount of concurrency in the execution of distributed software over a cluster is obtained. To address this problem, in this paper the program source code is analyzed to extract a performance evalua- tion function considering the characteristics of the cur- rent deployment of the distributed software. These char- acteristics include the number of available workstations, the number of processors of each workstation and the deployment constraints. According to these constraints, some of the objects must reside on specific workstations as they need to access hardware or software resources (such as Database, printer, file,…) on that workstation. The current researches in the Software Performance Engineering (S.P.E) are dedicated to estimate the per- formance of software in the early stage of development process due to needs for QOS. In order to achieve this goal, several works have been done to transform the software architectural models to the analyzable formal models. Some examples are deriving Queuing Network models from UML diagrams [6] or translating some of the UML diagrams to the Perti Nets [7,8]. In [9] con- structing and analyzing two kinds of performance models are proposed: software execution model and system exe- cution model. The former represents the software execu- tion behavior and is modeled by execution graphs and the latter is based on the queuing network models, which ![]() Applying Heuristic Search for Distributed Software Performance Enhancement145 represent the computer system platform, including hard- ware and software components. The software and system execution models are applied to assess the performance of the intended software architecture. There are also some related researches in the area of performance optimization of existing distributed pro- grams. In a mixed dynamic and programmer-driven ap- proach to optimize the performance of large-scale ob- ject-oriented distributed systems [10], an object-partit- ioning method is dynamically invoked at runtime to col- locate objects that communicate often. Here, the parti- tioning criterion is to gather objects that often communi- cate in a same partition. In a distribution strategy for program objects [11], an object graph construction algo- rithm and a partitioning policy for the program objects based on object types is presented. The distribution strategy is to place the most communicating objects in the same machine to minimize the network traffic. How- ever, when partitioning a program, in addition to mini- mizing the communication cost, the amount of concur- rency in the execution of the distributed partitions has to be maximized. To achieve this, in this paper, a new per- formance-driven partitioning criterion is proposed. The main contribution of this paper is to extend the Software Performance Engineering techniques to the reengineering area in order to optimize the performance of existing distributed software. To achieve this, a new parametric performance evaluation function to assess the performance of the current object deployment of the software over the computational nodes is presented. This function not only evaluates the current software deploy- ment but also proposes the best object deployment for the software. This function is automatically constructed while traversing the program call flow graph and consid- ers both blocking and non-blocking types for each invo- cation. The remaining parts of this paper are organized as fol- lows: in section 2 the main steps of the proposed method are described. Section 3 presents an optimization tech- nique called statement reordering which is applied to improve the amount of concurrency in the distributed program code. In section 4 the implementation of the proposed method is described. Finally the conclusions and future works are presented in section 5. 2. Software Performance Modeling To improve the performance of distributed software, some of the blocking invocations among objects should be transformed to non-blocking ones. The non-blocking invocations are implemented either by means of remote asynchronous calls (supported in some middlewares such as JavaSymphoney [12]) or Java Threads. However, an invocation is converted to non-blocking only when this conversion results in concurrent execution between the caller and callee considering the resultant overheads. Each non-blocking invocation incurs communication and initiation overheads. The former is the amount of re- quired time for sending the invocation parameters and receiving the return values between caller and calee. The latter is the amount of time required for starting the non-blocking invocation (such as asynchronous RMI or Java Threads). The object deployment for a given program is defined as pair (r,d). r denotes the set of all invocations in the program along with each invocation status (blocking or non blocking) an d denotes the deployment of caller and called objects, for each invocation Ii in r, over the avail- able computational nodes. The performance evaluation function Θ(r,d) estimates the amount of execution time for object deployment (r,d). The optimal object deploy- ment (ro,do) is the one for which the amount of Θ is minimum. To find the optimal object deployment all possible object deployment for the software should be evaluated using function Θ. However, this is a NP-Complete problem and should be solved using heu- ristic search algorithms such as Genetic algorithms. The deployment constraints must be considered during the search for the optimal object distribution. According to these constraints, some of the objects must reside on spe- cific workstations as they need to access hardware or software resources (such as Database, printer, file,…) on that workstation. We have used a Constrained Genetic Clustering algorithm to find the optimal object deploy- ment. In this algorithm function Θ is used as the search objective function. The main steps can be summarized as follows: Extracting Call Graph from the program source code. Unfolding the all graph to obtain the Call Tree. Extracting function Θ by analyzing the program source code. Search for the optimal object deployment (ro,do). This is achieved by using a genetic clustering algorithm that uses Θ as the search objective function. We have omitted the cycles in the extracted call graph to obtain the program call tree. This is necessary as in the call graph each node represents a class with multiple in- coming invocations. However at runtime each invocation may be performed using a separate instance of a class. Each object deployment (r,d) corresponds to a labeled partitioning of the program call tree. Each label of a node in the call tree specifies the workstation on which the object represented by that node resides. The status of invocations is determined by the partitioning. The invo- cations among partitions are assumed non-blocking while the invocations inside a partition are blocking. Therefore each labeled partitioning of the call tree specifies an ob- ject deployment (r,d) of the software uniquely and vice versa. To search for the optimal object deployment (ro,do) all possible labeled partitioning of the call tree are evalu- Copyright © 2009 SciRes JSEA ![]() Applying Heuristic Search for Distributed Software Performance Enhancement 146 ated by the constrained genetic clustering algorithm. The performance evaluation function Θ is built auto- matically while traversing a program call flow graph. A call flow graph represents the flow of method calls among program classes. Θ is built considering the esti- mated execution times of all instructions within the pro- gram code including the invocations. Since, the type of invocations affects the overall execution time of the pro- gram and is not determined until the program is parti- tioned; Θ is built as a general function including all the possible invocation types for each method call. There are four types of invocations: local blocking, remote block- ing (implemented using RMI), local non-blocking (im- plemented using Java Thread) and remote non-blocking (implemented using asynchronous RMI). For each type of invocation an overhead is defined as shown in Table 1. As described in the previous section, Θ maps each ob- ject distribution (r,d) to a value representing the esti- mated execution time of the distributed software with object distribution (r,d). Object distribution (r,d) is de- fined using a set of functions presented in Table 2 . For instance, consider a method invocation I1 that per- forms another invocation I2 during its execution. The estimated execution time of I1, denoted by TI1, when I2 type is (1) local blocking, (2) remote blocking,(3) local non-blocking and (4) remote non-blocking , is shown by relations (1) to (4) below respectively. Table1. Different overhead types α RMI initiation overhead β Thread creation overhead γ Asynch RMI initiation overhead Table 2. The maps that specify a labeled portioning and its equivalent object deployme nt (r,d) Φ Maps invocation Ii to a value 0 or 1, if Ii is inside a partition it is 1 otherwise it is 0. μ Maps invocation Ii to a value 0 or 1, if the caller and callee objects of Ii reside on the same workstation it is 0, otherwise it is 1. ω Maps invocation Ii to a workstation name on which Ii is executed Δ Maps invocation Ii to a value indicating the communication cost of the network link over which Ii is sending parameters or re- ceiving return values as a RMI or asyn- chronous RMI Г Maps each workstation w to the maximum number of threads created on it Π Maps each workstation w to the number of processing units installed on it TI1 = T 0 + TI2, TI2=T1 (1) TI1 = T 0 + α + TI2+ δ(I2) , TI2=T1 (2) TI1 = T 0 + β + S2 , S2=max (T1-d 2,0) (3) TI1 = T 0 + γ + S2 , S2=max (T 1+ δ(I2)-d 2,0) (4) Assuming that the target of the invocation I1 is method m, T0 is the total execution time of all the instructions excluding I2 within m. When an invocation such as I2 is non-blocking, the caller should wait for the results of I2 at some synchronization point during its execution. In relation (3) and (4), S2 denotes the amount of required time at the synchronization point of I2, to receive the re- sults of I2. The above relations can be combined as a sin- gle relation as follows: TI1= T0+Φ(I2)*(TI2 +μ(I2)*(α+δ(I2)) ) + (1-Φ(I2))*(S2+μ(I2)*γ+(1-μ(I2))*β) (5) S2=max(T1+μ(I2)* δ (I2) -d2,0) There may be several invocations, Ii, within method m and each invocation itself may include other invocations. Therefore, relation (5) for estimating the execution time of I1 can be generalized as follows: TI1 = T0 + Φ (Ii)*(TIi + μ(Ii)*( α+ δ(Ii)) ) + (1-Φ (Ii))*(Si + μ(Ii)*γ+(1-μ(Ii))*β) (6) Si= max(TIi+ μ(Ii)*δ(Ii) – di,0) In the above relation, Si denotes the time elapsed to wait for the results of the invocation Ii, di denotes the estimated execution time of the program statements lo- cated between each call statement, Ii, and the first loca- tions where the results of the call are required (the syn- chronization point of Ii). We can generalize relation (6) to obtain function Θ. this function that returns the estimated execution time for object deployment (r,d) is built by applying relations (6) recursively starting from the main() method of the pro- gram. Assuming that the program call flow graph is cy- cle-free, Θ (r,d) can always be computed recursively. However, there may be cycles in the call flow graph, resulting from direct or indirect recursive calls. Assum- ing that Ii is an invocation to a method in the cycle (and itself is not in the cycle) and the estimated number of recursions is ni then the estimated execution time of in- vocation Ii is multiplied by ni. An invocation Ii or a syn- chronization point Si may be located within a loop state- ment. Therefore to consider the impact of loop iterations on the time estimation, coefficients mi and ki have been added to relation (7): Θmain(r,d) = T0 +Φ(Ii)*ni*mi*(Θ Ii(r,d) +μ(Ii)*(α+δ(Ii)) )+ (1-Φ(Ii))* (7) (Si + μ(Ii)* γ+(1- μ(Ii))* β) Si= ki*max(Η(Ii)*Θ Ii(r,d)+ μ(Ii)*δ(Ii) – di,0) Copyright © 2009 SciRes JSEA ![]() Applying Heuristic Search for Distributed Software Performance Enhancement147 In the above relation, Θ Ii(r,d) denotes the estimated execution time of the program starting from invocation Ii. H(Ii) is the adjustment factor that adjusts the execution time of invocation Ii according to the hardware capacity of the workstation on which Ii will be executed. H(Ii) is defined as follows: H(Ii)= Г (ω(Ii)) / Π(ω(Ii)) (8) As described in Table 2, Г maps each workstation w to the number of possible threads created on it due to re- mote or local non-blocking invocations executed on w considering an object distribution (r,d). Г(w) for each workstation w in object distribution (r,d) is calculated as follows: Г(w)= Φ(Ii)*μ(Ii)+(1- Φ(Ii)) , (9) for all Ii such that: ω(Ii)=w 3. Statement Reordering In the preceding sections, the idea of partitioning the program call tree directed by Θ function was described. The main idea was to search for a partitioning of the program classes which results in the highest amount of concurrency among invocations. Obviously the concur- rency is achieved by performing some of the method invocations among actors asynchronously. Generally, converting an ordinary method call to an asynchronous one, poses two kinds of overheads on the execution: ini- tiation and communication (described earlier). The for- mer is denoted by s and the latter is denoted by O. Therefore, from the performance perspective, converting an ordinary call Ik to an asynchronous call is only benefi- cial when the sum of execution times of program instruc- tions located between Ii and its synchronization point Si , denoted by di, is greater than s+O. Obviously, the larger the amount of di, the more concurrency between the caller and the callee is obtained. Figure 1. The statement reordering However, a major difficulty is that programmers usu- ally use the results of any method call Ii immediately after (di=0). Therefore there will be no chance for con- currency when transforming method calls to asynchro- nous calls. To resolve the difficulty, we have applied the ideas of statement reordering to enhance the potential parallelism degree of a program by increasing the amount of di for each invocation. The algorithm attempts to insert as many statements as possible between an in- vocation and its first data-dependent statement, consid- ering the data dependencies between the statements. Ob- viously the reordering should be performed such that the original semantic of the program is preserved. To do this during the statement reordering the data and control de- pendency among statements must not be violated. Data and control dependency among program statements are represented by a acyclic graph called Task Graph [13]. The statement reordering is performed such that the de- pendencies represented by the program task graph are not violated. The overall steps including statement reordering is illustrated in Figure 1. In the statement reordering algorithm, the program statements are classified as follows: Call: a method invocation Use, statement which is data dependent on a Ca ll Common, an ordinary statement which is neither a Call nor a Use The algorithm comprises two main steps. In the first step the program statements are moved from the Original Code to the Reordered Code gradually. In this step, pre- sented in Figure 2, Call statements are moved to the re- ordered cod first and Use statements are moved as late as possible. In the second step, the reordered code resulted by applying the first step, is further optimized by reduc- ing the time elapsed to wait for the results of Call state- ments. This is achieved by inserting as many statements as possible between each Call and its corresponding Use. In the first step, Call statements of longer execution time are moved to the reordered code first because the longer the execution time of a Call, the more statements should be inserted between that Call and the correspond- ing Use. Obviously, before a statement is moved into the reordered code, all of its parent statements in the pro- gram task graph should be moved into the reordered code. In the second step, the reordered code resulted in the first step is further optimized. To achieve this, the time required to wait for the results of Call statements is minimized. This is achieved by pushing down Use statements with positive wait time as far as their wait time reaches zero. Here Use statements with longer wait time are selected and pushed down first. 4. Implementation We have developed a tool support that implements the Copyright © 2009 SciRes JSEA ![]() Applying Heuristic Search for Distributed Software Performance Enhancement 148 steps described in section 2 to find the optimal object distribution for distributed software. Within this envi- ronment, a Java source code analyzer implemented using COMPOST [14] library, analyzes the program source to extract the call graph, call flow graph, call tree and build the Θ function. To build the Θ function for a program, first the number of clock cycles for each statement in the program is determined, according to the JOP microin- struction definition [15], and saved in an XML document. JOP is an implementation of Java Virtual Machine in Algorithm Reorder (Task-Graph: DAG) OUT: Reordered-Code : List Begin 1. If there is no Call in Task-Graph which is not moved 2. While there is a Common-statement in Task-Graph which is not moved 3. Select a Common-statement whose predecessors in Task-Graph are already moved 4. Append this statement to Reordered-Code 5. Label this instruction as moved 6. End While 7. While there is a Use in Task-Graph which is not moved 8. Select a Use whose predecessors in Task-Graph are already moved 9. Append this instruction to Reordered-Code 10. Label this instruction as moved 11. End While 12. Else 13. Find a Call C with the longest execution time in Task-Graph which is not moved 14. Let New-Task-Graph be a Sub-graph of Task-Graph, including Predecessors of C 15. Reorder(New-Task-Graph) 16. Append C to Reordered-Code 17. Label C as moved 18. Reorder(Task-Graph) 19. End If End Algorithm Optimize (Task-Graph: DAG, Reordered-Code : List ) OUT: New-Reordered-Code: List Begin 1. While there is a Use in Task-Graph which is not selected 2. Select a Use U with the longest wait time W in Task-Graph 3. Find all unselected nodes in Task-Graph which are not connected to U through a path in the task graph and 4. Add them to Moving-List 5. Remove all those nodes from Moving-List which do not share an immediate control dependent parent with U 6. While W>0 7. Select an instruction I, from Moving-List, whose prede- cessors in Task-Graph are not in Moving-List 8. Let P be the set of immediate predecessors of I 9. Let C be a Call whose results are used by U 10. Let P=P {C} 11. Move I to a position after instructions in P and before U in Reordered-Code 12. Remove I from Moving-List 13. Subtract the estimated time of I form W End While End While End Figure 2. The statement reordering algorithms hardware. Our tool also inputs the loop bounds in the program as they are needed during the Θ generation. The generated XML document is applied by a statement re- ordering [13] engine to maximize the distances between each call statement and its very first data-dependent statement. The reordered program and the XML docu- ment are input to a separate module to produce the Θ function. The resultant Θ function is applied as an objec- tive function of a constrained genetic clustering algo- rithm [16] to find a near-optimal object deployment for the distributed software. A practical evaluation of the proposed method to op- timize the performance of distributed object-oriented programs is presented in this section. We have used two Java case-studies: TSP and Consolidated Clustering (CC). The first case study evaluates the impact of applying the proposed approach on a TSP program containing 18 classes and 129 method calls. This program finds near optimal Hamiltonian Circuit in a graph, using minimum spanning trees. The second case study measures the amount of speedup achieved by optimizing the perform- ance of a program called Consolidated Clustering [17]. Consolidated Clustering is a graph clustering application written in Java. This program comprises 16 classes and 23 method calls. In this program, a graph is clustered several times using heuristic clustering algorithms. The results of each clustering are stored in a database for fur- ther uses. This program consolidates the clustering re- sults to obtain a clustering with a specific confidence threshold. The program is relatively slow, because it ap- plies the heuristic algorithms for clustering. The case studies were analyzed first to extract function Θ for them. Then a genetic clustering algorithm was ap- plied to partition the call tree of each program to find the optimal object deployment using Θ as the objective func- tion. The chosen test bed was a cluster with 3 single processor Pentium computers running JavaSymphoney [12] as the cluster middleware. The parameter passing mechanism in remote non-blocking invocations in this test bed is implemented using copy-restore technique. Before applying the genetic clustering algorithm on the case-studies, the values for parameters α, β, γ and δ(Ii) were measured in the test bed. The amount of communi- cation cost over the Pentium cluster, regarding our un- derlying communication middleware, JavaSymphony, was measured less than 100 ms. To estimate the commu- nication cost in terms of JOP clock cycles, the number of clock cycles of a sample program was divided by the measured execution time of that program. According to this estimation, the communication cost δ(Ii) was nearly 107 clock cycles for all Ii. The measured amount of pa- rameters α, β and γ in the test bed were almost the same and was estimated 104 clock cycles. We applied a con- straint for the CC program as 3 of its classes in the call tree should necessarily reside on the workstation on Copyright © 2009 SciRes JSEA ![]() Applying Heuristic Search for Distributed Software Performance Enhancement Copyright © 2009 SciRes JSEA 149 Table 3. Measured spee dups 20 40 100 130 400 500 TSP:Θ 0.8 0.9 1 1.2 1.35 1.4 CC: Θ 0.6 0.7 0.8 1 1.3 1.4 TSP:MCC 0.3 0.5 0.6 0.7 0.78 0.8 TSP:TM 0.6 0.8 0.2 0.8 1 0.8 which the Data Base server was running. The measured speedups resulted from executing TSP and CC on the test bed are presented in Table 3. The re- sults are compared to other methods: (1) applying MCC function and (2) and applying trivial method. MCC de- notes Minimum Communication Cost described in sec- tion 1. In the trivial method, denoted by TM, the invoca- tions are assigned to a workstation with the lowest load at runtime for execution. 5. Conclusions In this paper the software reengineering research area has been extended to include performance improvement of existing distributed software. To achieve this first a per- formance assessment function is extracted from the pro- gram source code. Then this function is applied to find optimal object deployment of the software using a con- strained genetic clustering algorithm. The result is a la- beled partitioning of the program call tree. The invoca- tions inside a partition are assumed blocking while the invocations among partitions are non-blocking. The la- bels in the labeled partitioning graph indicate the work- stations on which objects reside. We have implemented this approach and applied that on two case studies. The result of our measurements shows this approach can be applied to improve the performance of legacy software. This is an ongoing research in the field of Software Performance Engineering. As the future work we intend to extend the idea of architectural level performance as- sessment in forward engineering to validate the software models in the sense that whether they satisfy the per- formance provisions or not. REFERENCES [1] B. Bellay and Gallh, “Reverse engineering to recover and describe a systems architecture,” Development and Evo- lution of Software Architectures for Product Families Lecture Notes in Computer Science, Vol. 1429, 1998. [2] D. R. Harris, H. B. Reubenstein and A.S.Yeh, “Reverse engineering to the architectural level,” Proc. 17th Int. Conf. Software Engineering, Seattle, Washington, US, 1995. [3] S. Parsa and O. Bushehrian, “The design and implemen- tation of a tool for automatic software modularization,” J. Supercomput., Vol. 32, No. 1, pp. 71–94, 2005. [4] B. S. Mitchell and M. Spiros, “Bunch: A clustering tool for the recovery and maintenance of software system structure,” Proc. Int. Conf. Software Maintenance, 1999. (IEEE) [5] L. Tahvildari, K. Kontoglannis and J. Mylopoulos, “Qual- itydriven software re-engineering,” J. Syst. Softw., Vol. 66, pp. 225–239, 2003. [6] Hyunsang Youn, Suhyeon Jang and Eunseok Lee, “De- riving queuing network model for UML for software per- formance prediction,” Fifth International Conference on Software Engineering Research, Management and Appli- cation, pp. 125–131, 2007. (IEEE) [7] J. M. Fernandes, S. Tjell, J. B. Jorgensen and O. R. Ribeiro, “Designing tool support for translating use cases and UML 2.0 sequence diagrams into a colored Petri Net,” Proc. 16th international Workshop on Scenarios and State Machines, 2007. (IEEE) [8] R. G. Pettit and H. Gomma, “Analyzing behavior of con- current software designs for embedded systems,” Proc. 10th International Symposium on Object and Compo- nent-Oriented Real-Time Distributed Computing, 2007. (IEEE) [9] Andolfif., F. Aquilani, S. Balsamo, and P. Inverardi, “De- riving performance models of software architectures from message sequence charts,” Proc. 2nd Int. Workshop on Software and Performance (WOSP2000), Canada, 2000. [10] Y. Gourhant, S. Louboutin, V. Cahill, A. Condon, G. Starovic, and B. Tangney, “Dynamic clustering in an ob- ject-oriented distributed system,” Proc. OLDA-II (Objects in Large Distributed Applications), Ottawa, Canada, Oc- tober 1992. [11] D. Deb, M. Fuad, and M. J. Oudshoom, “Towards auto- nomic distribution of existing object oriented programs,” Int. Conf. Autonomic and Autonomous Systems (ICAS’ 06), 2006. (IEEE) [12] T. Fahringer and A. Jugravu, “JavaSymphony: New di- rectives to control and synchronize locality, parallelism, and load balancing for cluster and GRID-computing,” Proc. Joint ACM Java Grande–ISCOPE 2002 Conf., Se- attle, Washington, November 2002. [13] S. Parsa and O. Bushehrian, “Genetic clustering with constraints,” Journal of research and practice in informa- tion technology, Vol. 39, No. 1, pp. 47–60, 2007. [14] http://www.info.uni-karlsruhe.de/~compost, last visit: 12th September 2009. [15] M. Schoeberl, “A time predictable Java processor,” Proc. Conf. Design, Automation and Test in Europe, Germany, pp. 800–805, 2006. [16] S. Parsa and O. Bushehrian, “Performance-driven object oriented program re-modularization,” Journal of IET Software, Vol. 2, No. 4, pp. 362–378, 2008. [17] B. S. Mitchell, “A heuristic search approach to solving the software clustering problem,” Ph.D Thesis, Drexel University, March 2002. |







