Version 20 of static data in command procedures

Updated 2006-09-21 13:15:19 by TR

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 Tcl_ThreadDataKey oneDatakey;
    Tcl_Obj **objPtrPtr = Tcl_GetThreadData(
        &oneDataKey, (int) sizeof (Tcl_Obj *));
    if (*objPtrPtr == NULL) {
      *objPtrPtr = Tcl_NewIntObj(1);
      Tcl_IncrRefCount(*objPtrPtr);
      Tcl_CreateThreadExitHandler(
          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);

In this alternative, the Tcl_Obj is shared by multiple interps in the same thread. For applications that use many interps in each thread, that can add up. In contrast to Alternative 3, the key value here is a Tcl_ThreadDataKey rather than a string, so it's much less likely to have any inadvertent collision in the key value with other code. Note, though, that after all [one] commands in all interps are deleted, the shared value will live on until the thread exits.

One nice feature of this alternative is that the ClientData and Tcl_CmdDeleteProc arguments to Tcl_CreateObjCommand are both NULL. That is, the technique is implemented entirely within the command procedure; no special initialization is required. This also means there's no conflict with other uses the command may have for those arguments.

This alternative could be modified to free the shared value when no more interps are using it (with the addition of a Tcl_CmdDeleteProc argument, etc.), but that would require storing in the ThreadData the set of interps currently sharing the shared value. For a single Tcl_Obj value, that additional overhead doesn't make sense, but the technique is available for cases where the shared, static data is more substantial, or when freeing an unused resource is otherwise of critical importance.


Comments on these, or other alternatives, are invited.

DGP


For a pure-Tcl solution, see also static variables


Category Discussion | Category Tcl Library