Version 15 of static data in command procedures

Updated 2004-10-05 03:19:47

Tcl offers several techniques that can be used to implement static data in command procedures. This page is intended to present the pros and cons.


Example:

Here is a command procedure for a simple Tcl command that makes use of a "static" value. Roughly, it's a constant value that each invocation of the command will use without changing, but a value that need not be created at all if the command is never evaluated.

  int
  OneObjCommand(ClientData cd, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
  {
    Tcl_SetObjResult(interp, Tcl_NewIntObj(1));
    return TCL_OK;
  }

  Tcl_CreateObjCommand(interp, "one", OneObjCommand, NULL, NULL);

Note that each time [one] is evaluated, a new Tcl_Obj is created to hold the value 1. Since Tcl is capable of managing shared Tcl_Objs, other alternatives are possible that would set the result to be an additional reference to one shared Tcl_Obj rather than a new one each time. The alternatives differ mostly on where that shared Tcl_Obj is stored between calls.

Why might we want an alternative? Most compelling reason is memory efficiency. Consider the script:

  for {set i 0} {$i < 1000000} {incr i} {
    set a($i) [one]
  }

With the implementation of [one] above, 1 million Tcl_Obj structs have to be allocated.


Alternative 1: ClientData

  int
  OneObjCommand(ClientData cd, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
  {
    Tcl_SetObjResult(interp, (Tcl_Obj *)cd);
    return TCL_OK;
  }
  void
  OneDelete(ClientData cd)
  {
    Tcl_Obj *objPtr = (Tcl_Obj *)cd;
    Tcl_DecrRefCount(objPtr);
  }

  objPtr = Tcl_NewIntObj(1);
  Tcl_IncrObjCount(objPtr);
  Tcl_CreateObjCommand(interp, "one", OneObjCommand,
    (ClientData)objPtr, OneDelete);

Here the shared Tcl_Obj with the value 1 is stuffed in the ClientData of the [one] command. One disadvantage of this alternative is that the shared Tcl_Obj is created whether or not [one] is ever called. For larger amounts of static data, that might be a waste worth avoiding. Another possible disadvantage might be conflict with other uses of the ClientData word that a command might have.


Alternative 2: One field in ClientData

  typedef struct OneData {
    Tcl_Obj *one;
  } OneData;
  int
  OneObjCommand(ClientData cd, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
  {
    OneData *dataPtr = (OneData *)cd;
    if (dataPtr->one == NULL) {
      dataPtr->one = Tcl_NewIntObj(1);
      Tcl_IncrRefCount(dataPtr->one);
    }
    Tcl_SetObjResult(interp, dataPtr->one);
    return TCL_OK;
  }
  void
  OneDelete(ClientData cd)
  {
    OneData *dataPtr = (OneData *)cd;
    if (dataPtr->one != NULL) {
      Tcl_DecrRefCount(dataPtr->one);
    }
    Tcl_Free(dataPtr);
  }

  dataPtr = Tcl_Alloc((int)sizeof(OneData));
  Tcl_CreateObjCommand(interp, "one", OneObjCommand,
    (ClientData)dataPtr, OneDelete);

This is a good choice when the command is already making use of ClientData.


Alternative 3: Interp AssocData

  int
  OneObjCommand(ClientData cd, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
  {
    Tcl_Obj *objPtr = Tcl_GetAssocData(interp, "one", NULL);
    if (objPtr == NULL) {
      objPtr = Tcl_NewIntObj(1);
      Tcl_IncrRefCount(objPtr);
      Tcl_SetAssocData(interp, "one",
          OneAssocDelete, (ClientData)objPtr);
    }
    Tcl_SetObjResult(interp, objPtr);
    return TCL_OK;
  }
  void
  OneAssocDelete(ClientData cd, Tcl_Interp *interp)
  {
    Tcl_Obj *objPtr = (Tcl_Obj *)cd;  
    Tcl_DecrRefCount(objPtr);
  }
  void
  OneDelete(ClientData cd)
  {
    Tcl_Interp *interp = (Tcl_Interp *)cd;
    Tcl_DeleteAssocData(interp, "one");
  }

  Tcl_CreateObjCommand(interp, "one", OneObjCommand,
    (ClientData)interp, OneDelete);

This alternative is best avoided on several grounds. First, it's vulnerable to a collision in the key name passed to the Tcl_*AssocData routines. Any other code that also wants to store data under the "one" key of this interp will interfere. Second, note that the ClientData of the command had to be set to interp just to enable OneDelete to call Tcl_DeleteAssocData. This ties up the ClientData and is a messy tangle for later code maintainers to understand. Third, it's likely that the performance cost of the Tcl_GetAssocData lookup (based on TCL_STRING_KEYS) will be at least as expensive as the original Tcl_NewIntObj.

This alternative could be simplified somewhat by dropping OneDelete and the command's ClientData. The price would be that the shared value would not be freed during command deletion, but would hang around until the entire interp is destroyed.


Alternative 4: ThreadData

  int
  OneObjCommand(ClientData cd, Tcl_Interp *interp,
    int objc, Tcl_Obj *const objv[])
  {
    static TclThreadDataKey oneDatakey;
    Tcl_Obj **objPtrPtr = Tcl_GetThreadData(
        &oneDataKey, (int) sizeof (Tcl_Obj *));
    if (*objPtrPtr == NULL) {
      *objPtrPtr = Tcl_NewIntObj(1);
      Tcl_IncrRefCount(*objPtrPtr);
      Tcl_CreateExitHandler(OneRelease, (ClientData)(*objPtrPtr));
    }
    Tcl_SetObjResult(interp, *objPtrPtr);
    return TCL_OK;
  }
  void
  OneRelease(ClientData cd)
  {
    Tcl_Obj *objPtr = (Tcl_Obj *)cd;
    Tcl_DecrRefCount(objPtr);
  }

  Tcl_CreateObjCommand(interp, "one", OneObjCommand, NULL, NULL);

DGP


For a pure-Tcl solution, see also static variables