◈ Netsurfer / perl.gg / Devel::Peek - X-Ray Vision for Perl Variables

Devel::Peek - X-Ray Vision for Perl Variables

This page shows how to inspect the internal storage of Perl variables using Devel::Peek. Perl keeps numbers and strings in the same variable, and this module lets you see exactly which form Perl cached and when it decided to convert between them. The example below prints the C-level structure of a simple scalar.
use Devel::Peek; my $x = 42; Dump($x);
SV = IV(0x7fa3b2c04e90) at 0x7fa3b2c04ea0 REFCNT = 1 FLAGS = (IOK,pIOK) IV = 42
That output is the internal representation of an integer scalar (IV). The flags tell you Perl already knows this is a number, so it skips string conversion when you do math.

Now do something with it as a string:

my $x = 42; my $s = "$x"; # force string context Dump($x);
SV = PVIV(0x7fa3b2c14280) at 0x7fa3b2c04ea0 REFCNT = 1 FLAGS = (IOK,POK,pIOK,pPOK) IV = 42 PV = 0x7fa3b2d05230 "42"\0 CUR = 2 LEN = 10
Now the variable holds both an integer and a string in the same memory slot. Perl computed the string form when you interpolated it, and it kept that cached copy so future string operations skip the work.

This is Devel::Peek. It shows you what Perl actually stores inside a variable, at the C structure level. When something behaves unexpectedly, this is where you go to find out why.

Part 1: THE DUMP FUNCTION

Devel::Peek is a debugging module that prints the C structure behind a Perl variable. When a script behaves unexpectedly, this is the place to look for the root cause. The main routine is Dump, and you pass it any variable to see its full internal state.
use Devel::Peek; my $num = 3.14; Dump($num);
SV = NV(0x7fa3b2c04e88) at 0x7fa3b2c04ea0 REFCNT = 1 FLAGS = (NOK,pNOK) NV = 3.14
NV means numeric value, and that is how Perl stores floating point numbers. The flags say NOK, which simply tells you the numeric slot is valid and ready to use.

A string:

my $str = "hello"; Dump($str);
SV = PV(0x7fa3b2c04e78) at 0x7fa3b2c04ea0 REFCNT = 1 FLAGS = (POK,pPOK) PV = 0x7fa3b2d05230 "hello"\0 CUR = 5 LEN = 10
PV means pointer value, and that is Perl's internal name for a string. POK means the string slot is valid. CUR tracks the current length, and LEN tracks the allocated buffer size.

The \0 at the end of the PV is the null terminator. Perl keeps it there for C compatibility, but it does not count toward the string length.

Part 2: READING THE FLAGS

The FLAGS line is the most important part of the dump. It tells you exactly what Perl thinks this variable is:
FLAG MEANING ------ ------------------------------------------ IOK Integer value is valid NOK Numeric (float) value is valid POK String (PV) value is valid pIOK Private: integer value is valid pNOK Private: numeric value is valid pPOK Private: string value is valid ROK Reference value is valid UTF8 String is stored as UTF-8
The "p" versions (pIOK, pNOK, pPOK) are internal flags. For debugging purposes, the non-p versions are what you care about.

When you see multiple flags like (IOK,POK,pIOK,pPOK), it means the variable has both an integer and a string representation cached. Perl will use whichever one the current operation needs without converting.

Part 3: THE DUAL-NATURED SCALAR

This is the big reveal. A Perl scalar can be a string AND a number simultaneously:
use Devel::Peek; my $x = "42"; Dump($x); say "--- after numeric use ---"; my $y = $x + 0; Dump($x);
SV = PV(0x...) at 0x... REFCNT = 1 FLAGS = (POK,pPOK) PV = 0x... "42"\0 CUR = 2 LEN = 10 --- after numeric use --- SV = PVIV(0x...) at 0x... REFCNT = 1 FLAGS = (IOK,POK,pIOK,pPOK) IV = 42 PV = 0x... "42"\0 CUR = 2 LEN = 10
After the first dump, $x is purely a string (POK only). After we use it as a number ($x + 0), Perl converts it to an integer and caches the result. Now it has both IV = 42 and PV = "42".

The next time you use $x as a number, Perl grabs the cached IV directly. No string-to-number conversion needed.

This caching is why Perl is faster than you might expect at repeated numeric operations on "number-like" strings.

Part 4: WHY == AND EQ GIVE DIFFERENT RESULTS

Here is a classic Perl confusion that Devel::Peek explains instantly:
my $a = "00"; my $b = "0"; say $a == $b ? "equal" : "not equal"; # equal (numeric) say $a eq $b ? "equal" : "not equal"; # not equal (string)
What is going on? Look at the internals:
use Devel::Peek; my $a = "00"; Dump($a);
SV = PV(0x...) at 0x... REFCNT = 1 FLAGS = (POK,pPOK) PV = 0x... "00"\0 CUR = 2 LEN = 10
The string "00" is stored as two characters. When == compares, it converts both to numbers: "00" becomes 0, "0" becomes 0, they are equal. When eq compares, it compares the raw strings: "00" vs "0", different lengths, not equal.

Neither answer is wrong. You asked two different questions. The == asked "are these the same number?" The eq asked "are these the same string?" Devel::Peek shows you why: the PV is "00", which is a different string from "0" but the same number as 0.

Part 5: SPOTTING THE UTF-8 FLAG

The UTF8 flag is critical for debugging encoding issues:
use Devel::Peek; my $bytes = "\xc3\xa9"; # raw bytes my $char = "\x{e9}"; # Unicode character Dump($bytes); say "---"; Dump($char);
SV = PV(0x...) at 0x... REFCNT = 1 FLAGS = (POK,pPOK) PV = 0x... "\303\251"\0 CUR = 2 LEN = 10 --- SV = PV(0x...) at 0x... REFCNT = 1 FLAGS = (POK,pPOK,UTF8) PV = 0x... "\303\251"\0 [UTF8 "\x{e9}"] CUR = 2 LEN = 10
Both have the same bytes in the PV. But the second one has the UTF8 flag set, and the dump shows the decoded character in brackets: [UTF8 "\x{e9}"].

With the UTF8 flag, length($char) returns 1 (one character). Without it, length($bytes) returns 2 (two bytes). Same bytes, different interpretation. The flag is the switch.

When you get garbled text or "Wide character" warnings, dump the variable. If the UTF8 flag is missing when it should be there (or present when it should not be), that is your bug.

Part 6: REFERENCE COUNTING

Every Perl variable has a reference count (REFCNT). It tracks how many things point to this value:
use Devel::Peek; my $x = "hello"; Dump($x); # REFCNT = 1 my $ref = \$x; Dump($x); # REFCNT = 2 { my $ref2 = \$x; Dump($x); # REFCNT = 3 } Dump($x); # REFCNT = 2 (ref2 went out of scope)
When REFCNT hits 0, Perl frees the memory. This is Perl's garbage collection: simple reference counting.

Circular references break this:

my $a = {}; my $b = {}; $a->{other} = $b; $b->{other} = $a; # REFCNT of both is 2 # when $a and $b go out of scope, REFCNT drops to 1, not 0 # memory leak!
Dump the structures and you can see the REFCNT never reaching zero. This is why Scalar::Util::weaken exists.

You can also get just the reference count without a full dump:

use Devel::Peek qw(SvREFCNT); my $x = "hello"; my $ref = \$x; say SvREFCNT($x); # 2

Part 7: ARRAYS AND HASHES

Dump works on arrays and hashes too:
use Devel::Peek; my @arr = (1, "two", 3.0); Dump(\@arr);
SV = IV(0x...) at 0x... REFCNT = 1 FLAGS = (TEMP,ROK) RV = 0x... SV = PVAV(0x...) at 0x... REFCNT = 1 FLAGS = () ARRAY = 0x... FILL = 2 MAX = 3 FLAGS = (REAL) Elt No. 0 SV = IV(0x...) at 0x... REFCNT = 1 FLAGS = (IOK,pIOK) IV = 1 Elt No. 1 SV = PV(0x...) at 0x... REFCNT = 1 FLAGS = (POK,pPOK) PV = 0x... "two"\0 CUR = 3 LEN = 10 Elt No. 2 SV = NV(0x...) at 0x... REFCNT = 1 FLAGS = (NOK,pNOK) NV = 3
FILL is the highest used index (2, meaning 3 elements). MAX is the allocated size (Perl over-allocates for growth). Each element is a separate SV with its own type and flags.

For hashes, you can see the bucket structure:

my %h = (name => "Larry", age => 69); Dump(\%h);
This shows the hash internals, including key-value pairs and the underlying hash table structure.

Part 8: PRACTICAL DEBUGGING

Problem: string comparison fails mysteriously.
my $a = get_from_database(); # returns "hello" my $b = get_from_user_input(); # returns "hello" if ($a eq $b) { say "match"; } else { say "no match"; # this fires. why? }
Dump both:
Dump($a); Dump($b);
SV = PV(0x...) at 0x... FLAGS = (POK,pPOK,UTF8) PV = 0x... "hello"\0 [UTF8 "hello"] SV = PV(0x...) at 0x... FLAGS = (POK,pPOK) PV = 0x... "hello"\0
One has the UTF8 flag, the other does not. Even though the bytes are identical, Perl compares them differently when the flags disagree. Fix it by normalizing both to the same encoding:
use Encode qw(decode encode); $a = decode('UTF-8', encode('UTF-8', $a)); $b = decode('UTF-8', encode('UTF-8', $b));
Or use utf8::upgrade on both.

Problem: number that is not a number.

my $val = "42\n"; # trailing newline from chomp failure say $val + 1; # 43, but with a warning Dump($val);
SV = PV(0x...) at 0x... FLAGS = (POK,pPOK) PV = 0x... "42\n"\0 CUR = 3
The PV shows "42\n". There it is. A hidden newline. The chomp was missed or failed. Devel::Peek makes invisible characters visible because it shows escape sequences in the PV dump.

Part 9: DUMPING COMPLEX STRUCTURES

For deep data structures, Dump gets verbose. You can control the depth:
use Devel::Peek; my $data = { users => [ { name => "Alice", age => 30 }, { name => "Bob", age => 25 }, ], }; Dump($data, 2); # only go 2 levels deep
The second argument limits recursion depth. Without it, Dump follows every reference to the bottom, which can produce thousands of lines for complex structures.

For quick overviews, sometimes Data::Dumper is more readable:

use Data::Dumper; say Dumper($data); # human-friendly structure dump
But Data::Dumper shows you the Perl-level view. Devel::Peek shows you the C-level view. When Data::Dumper says two things are the same but they behave differently, Devel::Peek tells you why.

They complement each other. Use Data::Dumper first to see the structure. Use Devel::Peek when something in the structure does not behave as expected.

Part 10: THE CONVERSION CACHE

The most important insight from Devel::Peek is understanding Perl's conversion caching.
use Devel::Peek; my $x = "123.45"; say "Step 1: string only"; Dump($x); my $y = $x + 0; say "Step 2: after numeric use"; Dump($x); my $z = "$x"; say "Step 3: after string use (already cached)"; Dump($x);
Step 1: string only FLAGS = (POK,pPOK) PV = "123.45" Step 2: after numeric use FLAGS = (NOK,POK,pNOK,pPOK) NV = 123.45 PV = "123.45" Step 3: after string use (already cached) FLAGS = (NOK,POK,pNOK,pPOK) NV = 123.45 PV = "123.45"
Step 1: just a string. Step 2: Perl computed the numeric value and cached it (NV appears alongside PV). Step 3: both representations already existed, no new conversion needed.

This is why Perl does not re-parse "123.45" every time you use it as a number. The first conversion is cached. Every subsequent use is a direct read from the cached slot.

It also explains a subtle behavior. Once a variable has been used as both a string and a number, it keeps both representations even if you change one:

my $x = "42"; $x + 0; # cache the IV $x = "99"; # assign a new string value Dump($x); # FLAGS = (POK,pPOK) - IV cache is invalidated # PV = "99" # The old IV = 42 is gone
Assignment clears the stale cache. Perl is not going to serve you yesterday's value from the numeric slot when you changed the string today.
Perl's internal variable structure: +---------------------------+ | SV (Scalar Value) | +---------------------------+ | REFCNT: 1 | | FLAGS: IOK, POK, UTF8 | +---------------------------+ | IV: 42 (integer) | | NV: 42.0 (float) | | PV: "42\0" (string) | | CUR: 2 (strlen) | | LEN: 10 (bufsize) | +---------------------------+ .--. |o_o | "Same variable. |:_/ | Three values. // \ \ Zero contradictions." (| | ) /'\_ _/`\ \___)=(___/
Devel::Peek is not a tool you use every day. It is a tool you use when nothing else explains the behavior. When Data::Dumper says two variables are identical but eq says they are not. When a number works in one context but not another. When UTF-8 is sometimes right and sometimes wrong and you cannot figure out why.

Those are Devel::Peek moments. You dump the variable, you see the flags, you see the cached values, and the mystery evaporates.

It is Perl's MRI machine. You do not use it for a checkup. You use it for a diagnosis.

perl.gg

✓ Document: Donehidden-gems / 19.2 KiB▧