The Surprising Cost of Type Checking in Eiffel

by Finnian Reilly (modified: 2023 Aug 30)

Introduction

If I asked you the question: given identical arguments of type EL_ZSTRING, which of the append routines in the code fragment below is faster, we would of course answer append_string. The reason is that in the case of append_string_general we can assume a small amount of overhead to do the two type checks.

  1. check if general conforms to READABLE_STRING_8
  2. check if general conforms to ZSTRING

However if I asked you to guess how much faster append_string is than append_string_general, I think we are likely to get a lot of wrong answers. Despite programming in Eiffel since 2000, I was surprised to discover that these two type checks add significant computational overhead. See benchmarks below.

deferred class EL_APPENDABLE_ZSTRING inherit EL_ZSTRING_IMPLEMENTATION feature {EL_READABLE_ZSTRING, STRING_HANDLER} -- Append strings append_string, append (s: EL_READABLE_ZSTRING) local old_count: INTEGER do old_count := count internal_append (s, old_count) if s.has_mixed_encoding then append_unencoded (s, old_count) end ensure new_count: count = old count + s.count inserted: elks_checking implies same_string (old (current_readable + s)) end append_string_general (general: READABLE_STRING_GENERAL) local offset: INTEGER do if attached {READABLE_STRING_8} general as str_8 and then cursor_8 (str_8).all_ascii then append_ascii (str_8) elseif attached {EL_ZSTRING} general as str_z then append_string (str_z) else offset := count accommodate (general.count) encode (general, offset) end ensure unencoded_valid: is_valid appended: substring (old count + 1, count).same_string_general (general) end

Benchmarks

I use the following code to benchmark the routines. The test data in Hexagram.English_titles consists of 64 strings with on average 62 characters each. Scroll down to see the results.

class APPEND_GENERAL_VS_APPEND inherit STRING_BENCHMARK_COMPARISON EL_SHARED_ZSTRING_CODEC create make feature -- Access Description: STRING = "{ZSTRING}.append_general VS append" feature -- Basic operations execute local list: EL_ZSTRING_LIST do create list.make_from_general (Hexagram.English_titles) compare ("compare append_general VS append", << ["append_general", agent append_general (list)], ["append", agent append (list)] >>) end feature {NONE} -- append append (string_list: EL_ZSTRING_LIST) local str: ZSTRING do create str.make (string_list.character_count) across string_list as list loop str.append (list.item) end end append_general (string_list: EL_ZSTRING_LIST) local str: ZSTRING do create str.make (string_list.character_count) across string_list as list loop str.append_string_general (list.item) end end end

Console Output

RESULTS: compare append_general VS append Passes over 500 millisecs (in descending order) append : 28112.0 times (100%) append_general : 20264.0 times (-27.9%)

Conclusion

Does a 28% reduction in performance seem like a lot to you? It does to me. I am assuming that it is the type checking that is slowing things down, and not the nested call to `append_string'. Or is there something I am missing ?

I will now think twice about doing type checking for performance critical routines.