Tcl_GetDoubleFromObj致C版列表类型判断性能低于Tcl版5倍的问题
Tcl C扩展性能优化:替代
Tcl_GetDoubleFromObj的类型判断方案 问题背景
需要遍历列表判断元素类型,并生成对应标记列表。先通过Tcl实现逻辑,再用C语言重写后发现性能比Tcl版慢5倍,定位到Tcl_GetDoubleFromObj函数占用大量时间,移除该函数及Tcl版对应判断后性能反转,希望找到C语言中不依赖该函数处理Tcl对象的方法。
Tcl实现代码
proc TCL_dataType {dataList} { set Tag {} foreach value $dataList { if {[string is double -strict $value]} { lappend Tag "N" } elseif {$value eq "null"} { lappend Tag "_" } else { lappend Tag "S" } } return $Tag }
C语言初始实现
int C_dataType (Tcl_Interp* interp, Tcl_Obj* data) { Tcl_Obj **dataList; int count; double d; if (Tcl_ListObjGetElements(interp, data, &count, &dataList) != TCL_OK) { return TCL_ERROR; } Tcl_Obj *Tag = Tcl_NewListObj (0,NULL); Tcl_Obj* s = Tcl_NewStringObj("S", 1); Tcl_Obj* n = Tcl_NewStringObj("N", 1); Tcl_Obj* null = Tcl_NewStringObj("_", 1); for (int i = 0; i < count; ++i) { if (Tcl_GetDoubleFromObj(interp, dataList[i], &d) == TCL_OK) { Tcl_ListObjAppendElement(interp, Tag, n); } else if (!strcmp(Tcl_GetString(dataList[i]), "null")) { Tcl_ListObjAppendElement(interp, Tag, null); } else { Tcl_ListObjAppendElement(interp, Tag, s); } } Tcl_SetObjResult(interp, Tag); return TCL_OK; }
性能测试代码及结果
proc randomValues {len} { set l {10 null foo bar} set randomList {} for {set i 0} {$i < $len} {incr i} { set index [expr {int(rand() * 4)}] lappend randomList [lindex $l $index] } return $randomList } set myRandomList [randomValues 10000] # time measure puts [time {C_dataType $myRandomList} 10] ; # 4921.3521 microseconds per iteration puts [time {TCL_dataType $myRandomList} 10] ; # 986.0601 microseconds per iteration
解决思路与优化实现
核心是直接检查Tcl对象的内部类型,跳过Tcl_GetDoubleFromObj的字符串解析过程,同时优化对象创建逻辑,提升性能。
优化后的C代码
int C_dataType(Tcl_Interp* interp, Tcl_Obj* data) { Tcl_Obj **dataList; int count; if (Tcl_ListObjGetElements(interp, data, &count, &dataList) != TCL_OK) { return TCL_ERROR; } Tcl_Obj *Tag = Tcl_NewListObj(0, NULL); // 创建标记对象并增加引用计数,避免循环内重复创建 Tcl_Obj* s = Tcl_NewStringObj("S", 1); Tcl_Obj* n = Tcl_NewStringObj("N", 1); Tcl_Obj* null_tag = Tcl_NewStringObj("_", 1); Tcl_IncrRefCount(s); Tcl_IncrRefCount(n); Tcl_IncrRefCount(null_tag); for (int i = 0; i < count; ++i) { Tcl_Obj *elem = dataList[i]; // 直接判断对象是否为已解析的数值类型 if (elem->typePtr == &Tcl_IntType || elem->typePtr == &Tcl_DoubleType) { Tcl_ListObjAppendElement(interp, Tag, n); } else { // 获取字符串表示(Tcl会缓存,不会重复转换) const char *str = Tcl_GetString(elem); if (!strcmp(str, "null")) { Tcl_ListObjAppendElement(interp, Tag, null_tag); } else { Tcl_ListObjAppendElement(interp, Tag, s); } } } // 释放引用计数 Tcl_DecrRefCount(s); Tcl_DecrRefCount(n); Tcl_DecrRefCount(null_tag); Tcl_SetObjResult(interp, Tag); return TCL_OK; }
关键优化点
- 直接判断对象内部类型:Tcl对象的
typePtr字段直接标识其类型,Tcl_IntType和Tcl_DoubleType是已解析的数值对象,无需再调用解析函数,省去字符串转数值的开销。 - 复用标记对象:提前创建三个标记对象并增加引用计数,避免在循环内重复创建字符串对象,减少内存分配和回收的开销。
- 减少不必要的转换:只有当对象不是数值类型时,才调用
Tcl_GetString获取字符串内容,且Tcl会缓存对象的字符串表示,不会重复执行转换操作。
特殊场景补充
如果需要处理字符串形式的数值(即对象是字符串类型但内容为数字),可以先通过字符串特征快速过滤(比如首字符是否为数字、正负号),再调用Tcl_GetDoubleFromObj解析,减少不必要的解析操作。但在当前测试场景中,数值对象已被Tcl解析为内置数值类型,直接判断类型即可满足需求。
内容的提问来源于stack exchange,提问作者Mkn
相关产品推荐
相关产品推荐

