Skip to content
VLSI Mentor

Tcl Part I · Module 7 · Dictionaries and Arrays

Arrays vs Dictionaries: Choosing One

Chapter 7.1 gave you a rule before you had seen either structure in anger. Now you have seen both completely, and the rule deserves a proper defence — including the parts where it is less clear-cut than it first looked. This chapter lays out the four axes that genuinely decide the question, measures the one everybody argues about, and is honest that the measurement does not matter nearly as much as people expect. It also deals with the situation most likely to come up in real work, which is that something else has already chosen for you.

Intermediate12 min readTclPart IDictionariesArraysDesign decisions

Part I · Module 7 · Chapter 7.11 · Arrays vs Dictionaries: Choosing One

1. The rule, restated

Use a dictionary when the data is something you pass, return, or store inside another structure. Use an array when you want variable-style access to named values inside a single scope — or when existing code hands you one.

Chapter 7.1 stated that before you had the vocabulary to evaluate it. Four things decide it, and they are not equally important.

2. Axis one: crossing a procedure boundary

This is the decisive one. A dictionary is a value, so it moves like any value:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc summarise {d} {
    return "[dict get $d mode] at width [dict get $d width]"
}

set cfg [dict create mode fast width 8]
puts [summarise $cfg]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fast at width 8

An array cannot do that. The equivalent needs the name and upvar:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc summarise_arr {name} {
    upvar 1 $name a
    return "$a(mode) at width $a(width)"
}

array set opt {mode fast width 8}
puts [summarise_arr opt]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fast at width 8

Both work. They are not equivalent in what they commit you to:

DictionaryArray
What the caller passesa valuea variable name
What the procedure can doread its own copyreach into the caller's scope
If the procedure modifies itcaller unaffectedcaller's data changes
Can the caller pass a literal?yesno
Can it be returned?yesonly flattened

The upvar form is not merely more typing — it is a different contract. The procedure now writes into its caller, which is harder to reason about and impossible to call with a value you just computed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts [summarise [dict create mode fast width 8]]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fast at width 8

There is no array equivalent of that line. You would have to assign to a temporary variable first.

3. Axis two: nesting

A dictionary goes inside a list, inside another dictionary, inside an array element:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set rows [list [dict create n u1 w 8] [dict create n u2 w 16]]
puts "[llength $rows] records, second is [dict get [lindex $rows 1] n]"

set store(cfg) [dict create width 8]
puts "inside an array element: [dict get $store(cfg) width]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2 records, second is u2
inside an array element: 8

An array goes nowhere. If your data is a collection of records, the records must be dictionaries — there is no choice to make.

4. Axis three: order

Dictionaries preserve insertion order. Arrays have no order at all.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set d [dict create]
foreach x {alpha beta gamma delta} { dict set d $x 1 }
puts "dict  : [dict keys $d]"

foreach x {alpha beta gamma delta} { set A($x) 1 }
puts "array : [array names A]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
dict  : alpha beta gamma delta
array : beta delta alpha gamma

If the order of your data carries meaning — the stages of a flow, the columns of a report, the sequence a user listed — a dictionary records it for free and an array loses it permanently. That is not something you can add back later: by the time you call array names, the information is gone.

5. Axis four: what the syntax makes easy

This is where arrays genuinely win. Counting:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach r {pass fail pass pass} { incr tally($r) }
puts [lsort [array names tally]]
puts "pass = $tally(pass)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fail pass
pass = 3

versus:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set t [dict create]
foreach r {pass fail pass pass} { dict incr t $r }
puts "pass = [dict get $t pass]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pass = 3

dict incr exists and is fine. But $tally(pass) reads like a variable because it is one, and in a block doing a lot of this, the array version has less punctuation per line. The same goes for append, and for info exists.

6. The axis everybody asks about: speed

Measured on this machine with time, 20,000 iterations over a 200-element structure:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set N 20000
array set A {}
set D [dict create]
for {set i 0} {$i < 200} {incr i} { set A(k$i) $i ; dict set D k$i $i }

puts "read  array : [time {set v $A(k100)}        $N]"
puts "read  dict  : [time {set v [dict get $D k100]} $N]"
puts "write array : [time {set A(k100) 7}         $N]"
puts "write dict  : [time {dict set D k100 7}     $N]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
read  array : 0.09926665 microseconds per iteration
read  dict  : 0.105375 microseconds per iteration
write array : 0.0770708 microseconds per iteration
write dict  : 0.3131979 microseconds per iteration

Reads are a tie — about six per cent apart, which is noise. Writes are not: the array is roughly four times faster, and that ratio reproduced across repeated runs.

Now the honest reading of those numbers. The difference on a write is about a quarter of a microsecond. To lose a tenth of a second to it you need around four hundred thousand writes. Below that, this measurement should not influence your choice at all — and if you are above it, you have a loop worth looking at for other reasons too.

7. When something else has already chosen

Often the decision is not yours.

Tcl hands you arrays. env and tcl_platform are arrays (Chapter 7.8 §7). You read them as arrays:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts "platform : $tcl_platform(platform)"
puts "PATH set : [info exists env(PATH)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
platform : unix
PATH set : 1

Existing code sets the convention. If a project's procedures all take an array name and upvar it, a single procedure that takes a dictionary is a seam everyone has to think about. Match the surrounding code; consistency is worth more than a marginally better structure.

A library's interface decides. If something hands you array get output, you have a dictionary-shaped list whether you wanted one or not — and that is exactly Chapter 7.12.

8. The decision, as a checklist

Work down the list and stop at the first match:

  1. Does it cross a procedure boundary as data? → dictionary.
  2. Does it go inside a list, a record, or another structure? → dictionary.
  3. Does its order mean anything? → dictionary.
  4. Did Tcl or existing code hand it to you as an array? → array.
  5. Is it scope-local tallying with names computed at run time? → array is comfortable; a dictionary is fine too.
  6. Anything else → dictionary.

Four of the six point one way, which is why "default to a dictionary" is reasonable shorthand — as long as you know what the exceptions are, rather than treating arrays as obsolete.

9. A worked comparison

The same job, both ways, so you can see the shape of each:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# Dictionary — the record is a value and comes back out
proc tally_dict {results} {
    set t [dict create]
    foreach r $results { dict incr t $r }
    return $t
}

set counts [tally_dict {pass fail pass pass skip}]
puts "returned: $counts"
puts "pass: [dict get $counts pass]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
returned: pass 3 fail 1 skip 1
pass: 3
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# Array — the procedure fills in the caller's variable
proc tally_array {results name} {
    upvar 1 $name t
    foreach r $results { incr t($r) }
}

tally_array {pass fail pass pass skip} counts2
puts "pass: $counts2(pass)"
puts "all : [lsort [array names counts2]]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pass: 3
all : fail pass skip

The dictionary version is a function: give it input, get a value. The array version is a procedure with an out-parameter. Both are legitimate Tcl; the first composes, and the second does not — you cannot write puts [tally_array ...].

Note too that the dictionary came back in the order the results first appeared, and the array needed lsort to be presentable at all.

10. Exercises

Exercise 1. For each, name the structure and the axis that decides it: (a) a per-file line count built in one procedure and printed sorted; (b) a parsed command line returned to the caller; (c) the stages of a flow, in order, each with settings; (d) env.

Exercise 2. Why can you write summarise [dict create a 1] but not the array equivalent?

Exercise 3. A procedure takes an array name, upvars it, and modifies it. Rewrite it as a dictionary version and state what changes for the caller.

Exercise 4. Measured here, a dictionary write took about 0.31 µs and an array write about 0.077 µs. How many writes before the difference reaches 0.1 s, and what does that imply?

Exercise 5. Someone argues "arrays are faster, so use arrays". Give two structural costs that outweigh the measurement.

Exercise 6. Write the same "count the words" job twice — once returning a dictionary, once filling a caller's array — and say which you would put in a shared library, and why.

Answers

1. (a) array — scope-local tallying, and it never leaves the procedure; (b) dictionary — it is returned, so it must be a value; (c) dictionary — the order carries meaning and the settings nest; (d) array — Tcl chose for you.

2. Because an array is not a value, so there is nothing to pass. The array form requires a name, and [dict create a 1] produces a value with no name attached. You would have to assign it to a variable first.

3.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# before
proc add_default_arr {name key value} {
    upvar 1 $name a
    if {![info exists a($key)]} { set a($key) $value }
}

# after
proc add_default_dict {d key value} {
    if {![dict exists $d $key]} { dict set d $key $value }
    return $d
}

set cfg [dict create mode fast]
set cfg [add_default_dict $cfg retries 1]
puts $cfg
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mode fast retries 1

The caller must now assign the result. In exchange, the procedure cannot modify anything it was not given, and it can be called on a value.

4. The difference is about 0.31 − 0.077 ≈ 0.23 µs per write, so 0.1 s needs roughly 0.1 / 0.00000023 ≈ 430,000 writes. It implies the measurement is irrelevant to almost every script, and that the choice should be made on structure.

5. Any two of: a dictionary can be returned from a procedure and an array cannot; a dictionary can be stored in a list or another record and an array cannot; a dictionary preserves insertion order and an array destroys it; a dictionary can be passed as a computed value with no intermediate variable.

6.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc count_dict {words} {
    set t [dict create]
    foreach w $words { dict incr t $w }
    return $t
}

proc count_arr {words name} {
    upvar 1 $name t
    foreach w $words { incr t($w) }
}

puts [count_dict {clk rst clk}]
count_arr {clk rst clk} c
puts "[lsort [array names c]] -> $c(clk)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clk 2 rst 1
clk rst -> 2

The dictionary version belongs in a shared library: it is a function, callers can compose it, it does not reach into anyone's scope, and it does not require its callers to agree on an out-parameter convention.

11. Remember

  • Four axes decide it: passing, nesting, order, and syntax comfort. Speed is a distant fifth.
  • A dictionary crosses procedure boundaries as data; an array requires a name and upvar, which is a different contract, not just more typing.
  • Only dictionaries nest. A collection of records means records that are dictionaries.
  • Dictionaries preserve insertion order; arrays destroy it irrecoverably.
  • Arrays make scope-local counting read better — incr tally($r) is a variable, not a lookup. That is the real argument for them.
  • Measured here: reads tie; array writes are about four times faster. The gap is around a quarter of a microsecond, so it needs hundreds of thousands of writes to matter.
  • Do not choose for speed. The structural costs of the wrong choice are far larger.
  • env, tcl_platform and existing project conventions decide for you. Match the code around you.

Where this fits

Part of the Tcl curriculum.