An operand is a value that an instruction uses to perform its operation. In YARV and other virtual machines, operands are known at compile-time and are built directly into the instruction sequence.
What is an Operand?
Operands are the “arguments” to VM instructions:
# Ruby code:
x = 42
# YARV bytecode:
putobject 42 # ← '42' is the operand
setlocal x # ← 'x' (local variable index) is the operand
The instruction putobject
needs to know what to push onto the stack. That “what” is the operand 42
.
Operands vs Stack Values
Important distinction in stack-based VMs:
Operands: Known at compile-time, embedded in instructions
putobject 100 # 100 is an operand (compile-time constant)
Stack Values: Computed at runtime, live on the stack
getlocal a # Gets 'a' (operand) and pushes its value (stack value)
getlocal b # Gets 'b' (operand) and pushes its value (stack value)
add # Pops two stack values, computes sum, pushes result
The add
instruction has no operands - it operates purely on stack values.
Types of Operands
1. Literal Values:
# Ruby:
x = 42
# YARV:
putobject 42 # Operand: literal integer 42
2. Variable References:
# Ruby:
y = x
# YARV:
getlocal_WC_0 x # Operand: local variable index/name
setlocal_WC_0 y # Operand: local variable index/name
3. Jump Targets:
# Ruby:
if condition
# code
end
# YARV:
getlocal condition
branchunless 10 # Operand: instruction offset (10)
# code
# label 10:
4. Call Data:
# Ruby:
object.method(arg)
# YARV:
opt_send_without_block <call_data> # Operand: call site metadata
The call data includes method name, argument count, and flags.
5. Symbol/String References:
# Ruby:
hash = { key: "value" }
# YARV:
putobject :key # Operand: symbol :key
putstring "value" # Operand: string "value"
newhash 2 # Operand: hash size (2)
Operand Encoding
YARV uses variable-width encoding for operands:
Short operands (fit in instruction):
putobject_INT2FIX_1 # Operand '1' encoded in instruction name
Long operands (follow instruction):
putobject <operand> # Operand follows as separate bytes
42 # ← Operand value here
This makes bytecode compact - common values use specialized instructions, rare values use general instructions with operands.
Operands in the Instruction Sequence
Operands are stored in the iseq:
Instruction Sequence Memory Layout:
┌─────────────┬──────────┬─────────────┬──────────┐
│ Instruction │ Operand │ Instruction │ Operand │ ...
├─────────────┼──────────┼─────────────┼──────────┤
│ putobject │ 42 │ setlocal │ x │ ...
└─────────────┴──────────┴─────────────┴──────────┘
↑ ↑
└── [[program counter|PC]] points here └── PC advances to here
The program counter advances past both instructions and their operands.
Constant Pool References
Some operands reference the constant pool:
# Ruby:
def greet
puts "Hello, World!"
end
# Constant pool in iseq:
# [0] = "Hello, World!"
# [1] = :puts
# [2] = <iseq:greet>
# Instructions:
# putstring @0 # Operand: index 0 in constant pool
# opt_send_without_block @1, ... # Operand: index 1 (method :puts)
The operand is an index into the pool, not the value itself. This avoids duplicating large values in the iseq.
Immediate vs Indirect Operands
Immediate operands - value directly in instruction:
putobject 1 # '1' stored immediately
putnil # No operand (nil is implicit)
Indirect operands - reference to elsewhere:
putstring @5 # Reference to constant pool slot 5
getlocal_WC_0 @2 # Reference to local variable slot 2
Operand Optimization
YARV optimizes operands:
Specialized Instructions:
# General form:
putobject 0 # Generic instruction + operand
# Optimized form:
putobject_INT2FIX_0 # Specialized instruction, no operand needed
Specialized instructions are faster - no operand to read, less instruction dispatch overhead.
Peephole Optimization:
# Before optimization:
putobject 2
putobject 2
add
# After optimization:
putobject 4 # Constant folding, new operand
The compiler pre-computes constant expressions and embeds the result as an operand.
Runtime Access
At runtime, the VM reads operands:
# Simplified VM execution loop
def execute(frame)
instruction = frame.iseq[frame.pc]
case instruction.type
when :putobject
operand = instruction.operand # Read operand
frame.stack.push(operand) # Use it
frame.pc += 1 # Advance past instruction
when :add
# No operands to read
b = frame.stack.pop
a = frame.stack.pop
frame.stack.push(a + b)
frame.pc += 1
end
end
The frame reads operands from the iseq as needed.
Operands and JIT Compilation
JIT compilers use operands to generate native code:
# Bytecode:
putobject 42
# YJIT generates (conceptual):
mov rax, 42 # Load immediate operand into register
push rax # Push to stack
The operand 42
becomes an immediate value in the generated machine code.
Debugging Operands
You can inspect operands:
# View bytecode with operands
ruby --dump=insns -e 'x = 42'
# Output shows:
# putobject 42 ← operand visible
# setlocal_WC_0 x ← operand visible
Tools like RubyVM::InstructionSequence#disasm
reveal operands clearly.