Difference between revisions of "Agent Calls"

m (Introduction)
m (Introduction)
Line 3: Line 3:
  
 
====Introduction====
 
====Introduction====
The Eiffel programming language supports closures with a agents. As a reminder, an agent is a normal Eiffel object of one of the types:
+
The Eiffel programming language supports closures with agents. As a reminder, an agent is a normal Eiffel object of one of the types:
 
*FUNCTION
 
*FUNCTION
 
*PROCEDURE
 
*PROCEDURE

Revision as of 00:47, 22 January 2007

Warning.png Warning: Warning: Article under development

Introduction

The Eiffel programming language supports closures with agents. As a reminder, an agent is a normal Eiffel object of one of the types:

  • FUNCTION
  • PROCEDURE

Some terminology is needed: The place where an agent is created (with the agent keyword or the tilde operator) is called the agent creation. The feature to which the agent points is the agent callee and the place (or places) where the agent is called is denoted agent call.

An example that clarifies these terms:

class
    TERMINOLOGY
 
feature
 
    f1
        local
            p: PROCEDURE [ANY, TUPLE]
        do
            p := agent target   -- agent creation
        end
 
    f2 (p: PROCEDURE [ANY, TUPLE])
        do
            p.call ([])         -- agent call
        end
 
    target                      -- callee
        do
        end
end

Terminology

In the following text the place where an agent is created (with the agent keyword or the tilde operator) is called the agent creation. The feature to which the agent points is the agent callee and the place (or places) where the agent is called is denoted agent call.

class 
    TERMINOLOGY
 
feature
 
    f1
        local
            p: PROCEDURE [ANY, TUPLE]
        do
            p := agent target   -- agent creation
        end
 
    f2 (p: PROCEDURE [ANY, TUPLE])
        do
            p.call ([])         -- agent call
        end
 
    target                      -- callee
        do
        end
end

Wrapper generation

Frozen code

Lets look first at frozen code. Class C is used throughout the following examples.

class
    C
 
feature
    f (p1, p2: INTEGER; p3: STRING): STRING
        do
            ...
        end   
end

For every agent creation a c-function is generated. This wrapper function does the following:

  • Reorder the closed and open arguments.
  • Calculate the proper agent callee based on the dynamic type of the target.
  • Call the agent callee

For the agent creation:

agent c.f (?, 1, ?)

the following wrapper function is generated:

EIF_REFERENCE _fAaatpmf_2_4 (EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE), 
                             EIF_TYPED_ELEMENT* closed, EIF_TYPED_ELEMENT* open)
{
    return (FUNCTION_CAST(EIF_REFERENCE, (EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE)) 
        RTVF(350, 30, "f", closed [1].element.rarg))(
            closed [1].element.rarg, 
            open [1].element.i32arg, 
            closed [2].element.i32arg, 
            open [2].element.rarg);
}

And for this agent creation:

agent {C}.f (1, ?, "hello")

The wrapper function looks like:

EIF_REFERENCE _fAaatpmf_1_3 (EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE), 
                             EIF_TYPED_ELEMENT* closed, EIF_TYPED_ELEMENT* open)
{
    return (FUNCTION_CAST(EIF_REFERENCE, (EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE)) 
        RTVF(350, 30, "f", open [1].element.rarg))(
            open [1].element.rarg, 
            closed [1].element.i32arg, 
            open [2].element.i32arg, 
            closed [2].element.rarg);
}

Finalized code

In finalized code things are only slightly different.

Whenever an agent is created with a closed target, the actual agent callee is calculated at agent creation time. Moreover, two wrappers are generated:

  • one which expects the closed arguments as a tuple (encapsulated). The only difference to the frozen wrapper is how the agent callee is calculated.
  • and one that expects them as separate parameters. We will later see how this wrapper is used to optimize some special agent calls.

For the agent creation:

agent c.f (?, 1, ?)

the following wrapper functions are generated:

EIF_REFERENCE _fAaatpmf_2_4 (EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE), 
                             EIF_TYPED_ELEMENT* closed, EIF_TYPED_ELEMENT* open)
{
    return f_ptr (closed [1].element.rarg, open [1].element.i32arg, closed [2].element.i32arg, open [2].element.rarg);
}
 
EIF_REFERENCE __fAaatpmf_2_4 (EIF_REFERENCE (*f_ptr)(EIF_REFERENCE, EIF_INTEGER_32, EIF_INTEGER_32, EIF_REFERENCE), 
                              EIF_TYPED_ELEMENT* closed, EIF_INTEGER_32 op_2, EIF_REFERENCE op_4)
{
    return f_ptr (closed [1].element.rarg, op_2, closed [2].element.i32arg, op_4);
}

The name of the wrapper for the encapsulated closed arguments always starts with one '_', whereas the other one starts with two underlines.

Melted agent creation

Of course it is possible that the agent creation is melted and hence there is no wrapper. For this situation the Eiffel runtime provides two generic wrapper functions:

RT_LNK void rout_obj_call_procedure_dynamic (
    int stype_id, int feature_id, int is_precompiled, int is_basic_type, int is_inline_agent, EIF_TYPED_ELEMENT* closed_args, 
    int closed_count, EIF_TYPED_ELEMENT* open_args, int open_count, EIF_REFERENCE open_map);
 
RT_LNK void rout_obj_call_function_dynamic (
    int stype_id, int feature_id, int is_precompiled, int is_basic_type, int is_inline_agent, EIF_TYPED_ELEMENT* closed_args, 
    int closed_count, EIF_TYPED_ELEMENT* open_args, int open_count, EIF_REFERENCE open_map, void* res);

These functions calculate the agent callee with the static type id and a feature id. Furthermore the reordering of open and closed arguments has to be done at agent call time with help of the open_map.

Agent call

To see how an agent call works it helps to look at the fast_item feature of class FUNCTION. In workbench mode, whenever there is a wrapper function (a_rout_disp /= Void) it is used to call the agent callee. If not, the generic wrapper (rout_obj_call_function_dynamic) from the runtime is called. In finalized mode there is always a wrapper.

fast_item (a_rout_disp, a_calc_rout_addr: POINTER
	       a_closed_operands: POINTER; a_operands: POINTER
	       a_class_id, a_feature_id: INTEGER; a_is_precompiled, a_is_basic, a_is_inline_agent: BOOLEAN
	       a_closed_count, a_open_count: INTEGER; a_open_map: POINTER): RESULT_TYPE
	external
	    "C inline use %"eif_rout_obj.h%""
	alias
	    "[
                #ifdef WORKBENCH
		    $$_result_type result;
		    if ($a_rout_disp != 0) {
			return (FUNCTION_CAST($$_result_type, (EIF_POINTER, EIF_REFERENCE, EIF_REFERENCE)) $a_rout_disp)(
			    $a_calc_rout_addr, $a_closed_operands, $a_operands);
		    } else {
			rout_obj_call_function_dynamic (
		     	    $a_class_id,
			    $a_feature_id,
			    $a_is_precompiled,
			    $a_is_basic,
			    $a_is_inline_agent,
			    $a_closed_operands,
			    $a_closed_count,
			    $a_operands,
			    $a_open_count,
			    $a_open_map, 
			    &result);
			return result;
		    }
		#else
		    return (FUNCTION_CAST($$_result_type, (EIF_POINTER, EIF_REFERENCE, EIF_REFERENCE)) $a_rout_disp)(
			$a_calc_rout_addr, $a_closed_operands, $a_operands);
		#endif
	    ]"
	end

Agent calls in final code

In finalized mode the compiler further optimizes agent calls if the following conditions hold:

  • The call is qualified and the target of the call is of type FUNCTION, PROCEDURE or PREDICATE.
  • The agent call is done with a call to item or call.

In this case, the compiler generates a direct call to the wrapper function.

Quite often an agent call is done with a manifest tuple:

caller (f: FUNCTION [ANY, TUPLE [INTEGER, STRING], STRING]): STRING
    do
	Result := f.item ([1, "hello"])
    end

This calls are further optimized by not generating the tuple but passing the elements of the tuple directly to the wrapper function. Thats why we need the second wrapper function in finalized mode. Feature caller is translated to the following c-code:

EIF_REFERENCE Fbnoo0v (EIF_REFERENCE Current, EIF_REFERENCE arg1)
{
    GTCX
    EIF_REFERENCE tp1 = NULL;
 
    tp1 = (FUNCTION_CAST(EIF_REFERENCE, (EIF_POINTER, EIF_REFERENCE, EIF_INTEGER_32, EIF_BOOLEAN)) *(EIF_POINTER *)(
        arg1+ @PTROFF(5,4,0,3,0,0)))(
	    *(EIF_POINTER *)(arg1+ @PTROFF(5,4,0,3,0,1)),
	    *(EIF_REFERENCE *)(arg1 + @REFACS(1)), ((EIF_INTEGER_32) 1L), (EIF_BOOLEAN) 1);
 
    return (EIF_REFERENCE)tp1;
}

Strategy for optimizing dotnet Agent Calls

Work in progress! Compared to the Classic version, dotnet Agent Calls are slow. The a short look at feature apply of class FUNCTION shows why:

apply 
        -- Call function with `operands' as last set.
    do
        last_result ?= rout_disp.invoke (target_object, internal_operands)
    end

The actual call is done through reflection. Additionally, in dotnet the two major optimizations are missing:

  • Directly passing the elements of manifest tuples as parameters. (And hence not creating the tuple)
  • Computing the reordering of closed and open operands at compile time. (By generating the proper wrapper)

All these three issues can be removed.

For every agent creation we create:

  • A delegate type that requires two Tuples (closed and open arguments) as arguments.
  • A static method that conforms to this delegate type. This method does the ordering of closed and open arguments and calls the agent callee.
  • A second delegate type, that requires one Tuple (closed arguments) and all the open arguments as arguments.
  • A second static method which conforms to the second delegate type and does the ordering and agent callee call.

The class ROUTINE will contain a reference of type