Skip to content

OptimizingModels

Baptiste Lesquoy edited this page Jan 14, 2025 · 11 revisions

Optimizing Models

This page aims at presenting some tips to optimize the memory footprint or the execution time of a model in GAMA.

Note: The optimizations presented here are only general ideas that work most of the time, but every model is different and sometimes those optimizations won't work or even produce worse execution time. It is thus highly recommended that you try them yourself on your setup and that you build test environment with the tools explained in the previous page (analysing code performance) to make sure that they are useful to your model.

Note 2: Some previously known optimizations from GAMA 1.6.1 and above, have become obsolete because they have been included in the compiler. They have, then, been removed from this page. For instance, writing 'rgb(0,0,0)' is now compiled directly as '#black'.

Scheduling

If you have a species of agents that, once created, are not supposed to do anything more (i.e. no behavior, no reflex, their actions triggered by other agents, their attributes being simply read and written by other agents), such as a "data" grid, or agents representing a "background" (from a shape file, etc.), consider using the schedules: [] facet on the definition of their species. This trick allows to tell GAMA to not schedule any of these agents.

grid my_grid height: 100 width: 100 schedules: []  {
      ...
}

The schedules facet is dynamically computed (even if the agents are not scheduled), so, if you happen to define agents that only need to be scheduled every x cycles, or depending on a condition, you can also write schedules to implement this. For instance, the following species will see its instances scheduled every 10 steps and only if a certain condition is met:

species my_species schedules: (every 10) ? (condition ? my_species : []) : []  {
     ...
}

In the same way, modelers can use the frequency facet to define when the agents of a species are going to be activated. By setting this facet to 0, the agents are never activated.

species my_species frequency: 0 {
     ...
}

Grid

Optimization Facets

In this section, we present some facets that allow to optimize the use of grid (in particular in terms of memories). Note that all these facet can be combined (see the Life model from the Models library).

use_regular_agents

If false, then a special class of agents is used. This special class of agents used less memories but has some limitation: the agents cannot inherit from a "normal" species, they cannot have sub-populations, their name cannot be modified, etc.

grid cell width: 50 height: 50 use_regular_agents: false ;

use_individual_shapes

If false, then only one geometry is used for all agents. This facet allows to gain a lot of memory, but should not be used if the geometries of the agents are often activated (for instance, by an aspect).

grid cell width: 50 height: 50 use_individual_shapes: false ;	

Parallel execution

The grid statement can also specify whether the agents of the grid are computed in parallel, using the facet parallel. This could increase (depending on the computation) the execution time. For more details about the pros and cons of doing so, please refer to the parallel section.

Operators

In GAMA, as in any other languages, some operators or order of execution of operators are more efficient than others. This section is dedicated to identify common mistakes and provide better alternative in the use of operators.

List operators

first_with

It is sometimes necessary to randomly select an element of a list that verifies a given condition. Many modelers use the one_of and the where operators to do this:

bug one_big_bug <- one_of (bug where (each.size > 10));

Whereas it is often more optimized to use the shuffle operator to shuffle the list, then the first_with operator to select the first element that verifies the condition:

bug one_big_bug <- shuffle(bug) first_with (each.size > 10);

where / count

It is quite common to want to count the number of elements of a list or a container that verify a condition. The obvious to do it is:

int n <- length(my_container where (each.size > 10));

This will however create an intermediary list before counting it, and this operation can be time consuming if the number of elements is important. To alleviate this problem, GAMA includes an operator called count that will count the elements that verify the condition by iterating directly on the container (no useless list created):

int n <- my_container count (each.size > 10);

Spatial operators

container of agents in closest_to, at_distance, overlapping, inside

Several spatial query operators (such as closest_to, at_distance, overlapping or inside) allow to restrict the agents being queried to a container of agents. For instance, one can write:

agent closest_agent <- a_container_containing_agents closest_to self;

This expression is formally equivalent to :

agent closest_agent <- a_container_containing_agent with_min_of (each distance_to self);

But it is much faster if your container is large, as it will query the agents using a spatial index (instead of browsing through the whole container). Note that in some cases, when you have a small number of agents, the first syntax will be faster. The same applies to the other operators.

Now consider a very common case: you need to restrict the agents being queried, not to a container, but to a species (which, actually, acts as a container in most cases). For instance, you want to know which predator is the closest to the current agent. If we apply the pattern above, we would write:

predator closest_predator <- predator with_min_of (each distance_to self);

or

predator closest_predator <- list(predator) closest_to self;

But these two operators can be painfully slow if your species has many instances (even in the second form). In that case, always prefer using directly the species as the left member:

predator closest_ predator <- predator closest_to self;

Not only is the syntax clearer, but the speed gain can be phenomenal because, in that case, the list of instances is not used (we just check if the agent is an instance of the left species).

However, what happens if one wants to query instances belonging to 2 or more species? If we follow our reasoning, the immediate way to write it would be (if predator 1 and predator 2 are two species):

agent closest_agent <- (list(predator1) + list(predator2)) closest_to self; 

or, more simply:

agent closest_agent <- (predator1 + predator2) closest_to self;

The first syntax suffers from the same problem than the previous syntax: GAMA has to browse through the list (created by the concatenation of the species populations) to filter agents. The solution, then, is again to use directly the species, as GAMA is clever enough to create a temporary "fake" population out of the concatenation of several species, which can be used exactly like a list of agents, but provides the advantages of a species population (no iteration made during filtering).

Accelerate closest_to with a first spatial filtering

The closest_to operator can sometimes be slow if numerous agents are concerned by this query. If the modeler is just interested in a small subset of agents, it is possible to apply a first spatial filtering on the agent list by using the at_distance operator.

For example, if the modeler wants first to do a spatial filtering of 10m:

agent closest_agent <- (predator1 at_distance 10) closest_to self;

To be sure to find an agent, the modeler can use a test statement:

agent closest_agent <- (predator1 at_distance 10) closest_to self;
if (closest_agent = nil) {closest_agent  <- predator1 closest_to self;}

Displays

shape

It is quite common to want to display an agent as a circle or a square. A common mistake is to mix up the shape to draw and the geometry of the agent in the model. If the modeler just wants to display a particular shape, he/she should not modify the agent geometry (i.e. its shape attribute, which is a point by default), but just specify the shape to draw in the agent aspect.

species bug {
     int size <- rnd(100);
	
      aspect circle {
          draw circle(2) color: #blue;
      }
}

circle vs square / sphere vs cube

Note that in OpenGL (3D) and Java2D (2D), the two rendering subsystems used in GAMA, creating and drawing a circle geometry is more time consuming than creating and drawing a square (or a rectangle). In the same way, drawing a sphere is more time consuming than drawing a cube. Hence, if you want to optimize your model displays and if the rendering does not explicitly need "rounded" agents, try to use squares/cubes rather than circles/spheres.

OpenGL refresh facets

In OpenGL display, it is possible to specify that it is not necessary to refresh a layer with the facet refresh. If in a species, the properties used for visualization (location, shape or color) are never modified, you can set refresh to false. Example:

display city_display_opengl type: opengl{
     species building aspect: base refresh: false;
     species road aspect: base refresh: false;
     species people aspect: base;
}

Manipulating containers and species

Manipulating containers (lists, maps etc.) and agents are usually the core of a model and the most critical parts of the code in terms of performances.

parallel

It is possible to execute the reflexes of agents of a species in parallel threads. This can greatly improve the execution time of a model as by default agents are executed one after another. To activate parallel execution of agents you just need to set the parallel facet of the species (or grid) to true:

species dummy_species parallel:true{

}
grid my_grid parallel:true{

}

Note: By default this option is not activated because we cannot guaranty the reproducibility of an experiment if it is. It implies that we do not know in advance which agent is going to be executed first, this also means that if your agents are meant to be executed in a certain order, this could break your model. Take for example this model where each cell of a grid has an effect on its neighbouring cells ,here disabling them, each disabled cell will be represented by the red color and enabled cells by the green color:

model parallel

grid my_grid parallel:false width:5 height:5 {
	
	bool to_be_executed <- true;
	rgb color <- #green update:to_be_executed ? #green : #red;
	reflex cancel_neighbours {
		if to_be_executed{
			write "I am cell '" + name + "' and my reflexes are executed";
			loop c over:neighbors{
				c.to_be_executed <- false;
			}			
		}
		to_be_executed <- true; // we reset for the next cycle
	}
}

experiment a {
	
	output{
		display main type:2d antialias:false{			
			grid my_grid border:#black;
		}
		
	}
}

With parallel set to false (the default) the order of execution of each cell is always the same and can be predicted to yield this display after the first cycle: image But if you change its value to true you can end up with this: image And it can change every cycle (or not) and there's no way to know in advance how it's going to look.

Iterating over containers

In gama there are multiple ways of iterating over containers and agents:

  • the ask statement to iterate over agents
  • the loop statement to iterate over anything, with its multiple syntaxes:
    • times iterates a certain number of times
    • over iterates directly over a list
    • from and to provide an index to iterate in a range of values
    • until iterates until a certain condition is met
  • the different container operators (such as collect or where) provide shortcuts for generic list manipulation tasks

In general the specialized operators (see this page in the documentation for the full list) are way more efficient, followed by ask and the loop over, then comes the other loop syntaxes. So as a general rule of thumb you should use as much as possible the container-related operators instead of the generic loop and ask statements. Here is an example model that showcases the difference in execution time of the different methods to sum the value of a property of all the agents in the simulation:

model accessinglistitems

global{
	int nb_agents <- 50000;	
}

species b{
	
	int v;
	
	init {
		v <- rnd(0, 10);
	}
}

experiment e {

	parameter "number of agents" var:nb_agents;

	reflex fill_list_from_agents {
		
		write "Start benchmarking with " + nb_agents + " agents";
		
		// we reset the agents
		ask b{
			do die;
		}
		create b number:nb_agents;
		
		int s1 <- 0;
		benchmark "sum with loop over" repeat:100{
			s1 <- 0;
			loop obj over:b{
				s1 <- s1 + obj.v;
			}
		}
		
		int s2 <- 0;
		benchmark "sum with loop from to" repeat:100{
			int to <- length(b)-1;
			s2 <- 0;
			loop i from:0 to:to{
				s2 <- s2 + b[i].v;
			}
		}
		
		int s3 <- 0;
		benchmark "sum with loop times" repeat:100{
			int to <- length(b);
			int i <- 0;
			s3 <- 0;
			loop times:to{
				s3 <- s3 + b[i].v;
				i <- i + 1;
			}
		}
		
		int s4 <- 0;
		benchmark "sum with ask"  repeat:100{
			s4 <- 0;
			ask b{
				s4 <- s4 + v;
			}
		}
		
		int s5 <- 0;
		benchmark "sum with collect" repeat:100{
			s5 <- sum(b collect (each.v));
		}
		
		// we check that all methods yield the same result
		assert s1 = s2;
		assert s2 = s3;
		assert s3 = s4;
		assert s4 = s5;
		
	}
}

Which gives results similar to this:

image

Big string manipulation

String manipulation, and especially concatenation (adding two strings together) is harmless for performances in normal condition, but once strings become big enough (thousands of characters), every operation becomes extremely costly. Moreover, the execution time of concatenation seem to be an exponential function of the size of the string. Take for example this model:

model stringconcat

experiment test {
	
	reflex concat {
		string s;
		int nb_concat <- 100;
		loop times:nb_concat{
			s <- s + rnd(0,10);
		}
		
	}
}

We are simply concatenating random digits into a string, ending up with a 100 character string. So far so good, this is good enough and works flawlessly on most machines. Now change nb_concat to 100000 and the operation starts to be non-negligible (for example it takes more than half a second on my computer). We can modify a bit the model to have a precise value of how long it takes:

model stringconcat

experiment test {
	
	
	action concatenate_string(int nb){
		string s <- '';			
		loop times:nb{
			s <- s + rnd(0,10);
		}
	}
	reflex concat {
		
		benchmark "concatenating 100 000 times" repeat:10  {
			do concatenate_string(100000);			
		}
		
		benchmark "concatenating 200 000 times" repeat:10  {
			do concatenate_string(200000);			
		}
		
		benchmark "concatenating 300 000 times" repeat:10  {
			do concatenate_string(300000);			
		}
		
	}
}

With that model you should notice that 200 000 characters is significantly slower than 100 000, in my case it was more than 4 times slower, and 300 000 even more as it was almost 10 times slower. Results on my computer looked like this:

100 000 characters 200 000 characters 300 000 characters
619 2563 5749

image

This can be particularly annoying because creating big string is itself a way to optimize outputing data from a model (it is often faster to write a big string once than small strings many times).

To help with string concatenation, it is advised to use the concatenate operator: instead of concatenating many times your text into one string, append each small string into a list of string, and when all the component are collected, merge them only once with the operator. Let's modify the previous model to see how it works:

model stringconcat

experiment test {
	
	
	action concatenate_string(int nb){
		list<string> list_strings <- [];			
		loop times:nb{
			list_strings <+ string(rnd(0,10));
		}
		string s <- concatenate(list_strings);
	}
	reflex concat {
		
		benchmark "concatenating 100 000 times" repeat:10  {
			do concatenate_string(100000);			
		}
		
		benchmark "concatenating 200 000 times" repeat:10  {
			do concatenate_string(200000);			
		}
		
		benchmark "concatenating 300 000 times" repeat:10  {
			do concatenate_string(300000);			
		}
		
	}
}

On the same computer as in the previous example I get those results:

100 000 characters 200 000 characters 300 000 characters
15 35 45

Now let's compare them:

100 000 characters 200 000 characters 300 000 characters
619 2563 5749
15 35 45

image

Not only it is orders of magnitude faster, but it is also growing linearly instead of exponentially which is more sustainable in case I want to concatenate even bigger strings in the future.

Threads

Since GAMA 1.9.1, a new skill has been added to implement threads in agents. The idea is not to give full control of threads to parallelize operations inside the simulation (though this could be achieved too), but to provide a way to communicate with the outside without blocking the execution of your simulation. GAMA simulation running mostly on one thread, having some heavy asynchronous operation running in a separate thread could greatly improve the simulation time, for example you can use threads to push the state of your simulation to an external web API every 10 minutes.

Note: Using threads to interact with your simulation could completely break its reproducibility in a similar way as it is explained in the parallel section.

  1. What's new (Changelog)
  1. Installation and Launching
    1. Installation
    2. Launching GAMA
    3. Updating GAMA
    4. Installing Plugins
  2. Workspace, Projects and Models
    1. Navigating in the Workspace
    2. Changing Workspace
    3. Importing Models
  3. Editing Models
    1. GAML Editor (Generalities)
    2. GAML Editor Tools
    3. Validation of Models
  4. Running Experiments
    1. Launching Experiments
    2. Experiments User interface
    3. Controls of experiments
    4. Parameters view
    5. Inspectors and monitors
    6. Displays
    7. Batch Specific UI
    8. Errors View
  5. Running Headless
    1. Headless Batch
    2. Headless Server
    3. Headless Legacy
  6. Preferences
  7. Troubleshooting
  1. Introduction
    1. Start with GAML
    2. Organization of a Model
    3. Basic programming concepts in GAML
  2. Manipulate basic Species
  3. Global Species
    1. Regular Species
    2. Defining Actions and Behaviors
    3. Interaction between Agents
    4. Attaching Skills
    5. Inheritance
  4. Defining Advanced Species
    1. Grid Species
    2. Graph Species
    3. Mirror Species
    4. Multi-Level Architecture
  5. Defining GUI Experiment
    1. Defining Parameters
    2. Defining Displays Generalities
    3. Defining 3D Displays
    4. Defining Charts
    5. Defining Monitors and Inspectors
    6. Defining Export files
    7. Defining User Interaction
  6. Exploring Models
    1. Run Several Simulations
    2. Batch Experiments
    3. Exploration Methods
  7. Optimizing Models
    1. Runtime Concepts
    2. Analyzing code performance
    3. Optimizing Models
  8. Multi-Paradigm Modeling
    1. Control Architecture
    2. Defining Differential Equations
  1. Manipulate OSM Data
  2. Cleaning OSM Data
  3. Diffusion
  4. Using Database
  5. Using FIPA ACL
  6. Using BDI with BEN
  7. Using Driving Skill
  8. Manipulate dates
  9. Manipulate lights
  10. Using comodel
  11. Save and restore Simulations
  12. Using network
  13. Headless mode
  14. Using Headless
  15. Writing Unit Tests
  16. Ensure model's reproducibility
  17. Going further with extensions
    1. Calling R
    2. Using Graphical Editor
    3. Using Git from GAMA
  1. Built-in Species
  2. Built-in Skills
  3. Built-in Architecture
  4. Statements
  5. Data Type
  6. File Type
  7. Expressions
    1. Literals
    2. Units and Constants
    3. Pseudo Variables
    4. Variables And Attributes
    5. Operators [A-A]
    6. Operators [B-C]
    7. Operators [D-H]
    8. Operators [I-M]
    9. Operators [N-R]
    10. Operators [S-Z]
  8. Exhaustive list of GAMA Keywords
  1. Installing the GIT version
  2. Developing Extensions
    1. Developing Plugins
    2. Developing Skills
    3. Developing Statements
    4. Developing Operators
    5. Developing Types
    6. Developing Species
    7. Developing Control Architectures
    8. Index of annotations
  3. Introduction to GAMA Java API
    1. Architecture of GAMA
    2. IScope
  4. Using GAMA flags
  5. Creating a release of GAMA
  6. Documentation generation

  1. Predator Prey
  2. Road Traffic
  3. 3D Tutorial
  4. Incremental Model
  5. Luneray's flu
  6. BDI Agents

  1. Team
  2. Projects using GAMA
  3. Scientific References
  4. Training Sessions

Resources

  1. Videos
  2. Conferences
  3. Code Examples
  4. Pedagogical materials
Clone this wiki locally