diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go
index 88e3615d3..1a20308f2 100644
--- a/pkg/core/blockchain.go
+++ b/pkg/core/blockchain.go
@@ -1917,8 +1917,9 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
 	if vm.HasFailed() {
 		return 0, fmt.Errorf("%w: vm execution has failed: %v", ErrVerificationFailed, err)
 	}
-	resEl := vm.Estack().Pop()
-	if resEl != nil {
+	estack := vm.Estack()
+	if estack.Len() > 0 {
+		resEl := estack.Pop()
 		res, err := resEl.Item().TryBool()
 		if err != nil {
 			return 0, fmt.Errorf("%w: invalid return value", ErrVerificationFailed)
diff --git a/pkg/vm/bench_test.go b/pkg/vm/bench_test.go
new file mode 100644
index 000000000..c0403ea8b
--- /dev/null
+++ b/pkg/vm/bench_test.go
@@ -0,0 +1,50 @@
+package vm
+
+import (
+	"encoding/base64"
+	"strconv"
+	"testing"
+
+	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
+	"github.com/stretchr/testify/require"
+)
+
+func benchScript(t *testing.B, script []byte) {
+	for n := 0; n < t.N; n++ {
+		t.StopTimer()
+		vm := load(script)
+		t.StartTimer()
+		err := vm.Run()
+		t.StopTimer()
+		require.NoError(t, err)
+		t.StartTimer()
+	}
+}
+
+// Shared as is by @ixje once upon a time (compiled from Python).
+func BenchmarkScriptFibonacci(t *testing.B) {
+	var script = []byte{87, 5, 0, 16, 112, 17, 113, 105, 104, 18, 192, 114, 16, 115, 34, 28, 104, 105, 158, 116, 106, 108, 75,
+		217, 48, 38, 5, 139, 34, 5, 207, 34, 3, 114, 105, 112, 108, 113, 107, 17, 158, 115, 107, 12, 2, 94, 1,
+		219, 33, 181, 36, 222, 106, 64}
+	benchScript(t, script)
+}
+
+func BenchmarkScriptNestedRefCount(t *testing.B) {
+	b64script := "whBNEcARTRHAVgEB/gGdYBFNEU0SwFMSwFhKJPNFUUVFRQ=="
+	script, err := base64.StdEncoding.DecodeString(b64script)
+	require.NoError(t, err)
+	benchScript(t, script)
+}
+
+func BenchmarkScriptPushPop(t *testing.B) {
+	for _, i := range []int{4, 16, 128, 1024} {
+		t.Run(strconv.Itoa(i), func(t *testing.B) {
+			var script = make([]byte, i*2)
+			for p := 0; p < i; p++ {
+				script[p] = byte(opcode.PUSH1)
+				script[i+p] = byte(opcode.DROP)
+			}
+			benchScript(t, script)
+		})
+	}
+}
diff --git a/pkg/vm/context.go b/pkg/vm/context.go
index e682cda1b..25d29f2fb 100644
--- a/pkg/vm/context.go
+++ b/pkg/vm/context.go
@@ -35,8 +35,8 @@ type Context struct {
 	local     *Slot
 	arguments *Slot
 
-	// Exception context stack pointer.
-	tryStack *Stack
+	// Exception context stack.
+	tryStack Stack
 
 	// Script hash of the prog.
 	scriptHash util.Uint160
@@ -282,10 +282,11 @@ func (c *Context) IsDeployed() bool {
 // getContextScriptHash returns script hash of the invocation stack element
 // number n.
 func (v *VM) getContextScriptHash(n int) util.Uint160 {
-	element := v.Istack().Peek(n)
-	if element == nil {
+	istack := v.Istack()
+	if istack.Len() <= n {
 		return util.Uint160{}
 	}
+	element := istack.Peek(n)
 	ctxIface := element.Value()
 	ctx := ctxIface.(*Context)
 	return ctx.ScriptHash()
diff --git a/pkg/vm/debug_test.go b/pkg/vm/debug_test.go
index 7e6a8877e..c30964b82 100644
--- a/pkg/vm/debug_test.go
+++ b/pkg/vm/debug_test.go
@@ -20,7 +20,7 @@ func TestVM_Debug(t *testing.T) {
 		require.NoError(t, v.Run())
 		require.Equal(t, 5, v.Context().NextIP())
 		require.NoError(t, v.Run())
-		require.Equal(t, 1, v.estack.len)
+		require.Equal(t, 1, v.estack.Len())
 		require.Equal(t, big.NewInt(5), v.estack.Top().Value())
 	})
 	t.Run("StepInto", func(t *testing.T) {
@@ -29,14 +29,14 @@ func TestVM_Debug(t *testing.T) {
 		require.Equal(t, 3, v.Context().NextIP())
 		require.NoError(t, v.StepOut())
 		require.Equal(t, 2, v.Context().NextIP())
-		require.Equal(t, 1, v.estack.len)
+		require.Equal(t, 1, v.estack.Len())
 		require.Equal(t, big.NewInt(5), v.estack.Top().Value())
 	})
 	t.Run("StepOver", func(t *testing.T) {
 		v := load(prog)
 		require.NoError(t, v.StepOver())
 		require.Equal(t, 2, v.Context().NextIP())
-		require.Equal(t, 1, v.estack.len)
+		require.Equal(t, 1, v.estack.Len())
 		require.Equal(t, big.NewInt(5), v.estack.Top().Value())
 	})
 }
diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go
index 659ff0191..f94023f34 100644
--- a/pkg/vm/stack.go
+++ b/pkg/vm/stack.go
@@ -9,70 +9,36 @@ import (
 	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
 )
 
-// Stack implementation for the neo-go virtual machine. The stack implements
-// a double linked list where its semantics are first in first out.
-// To simplify the implementation, internally a Stack s is implemented as a
-// ring, such that &s.top is both the next element of the last element s.Back()
-// and the previous element of the first element s.Top().
-//
-// s.Push(0)
-// s.Push(1)
-// s.Push(2)
-//
-// [ 2 ] > top
-// [ 1 ]
-// [ 0 ] > back
-//
-// s.Pop() > 2
-//
-// [ 1 ]
-// [ 0 ]
+// Stack implementation for the neo-go virtual machine. The stack with its LIFO
+// semantics is emulated from simple slice where the top of the stack corresponds
+// to the latest element of this slice. Pushes are appends to this slice, pops are
+// slice resizes.
 
-// Element represents an element in the double linked list (the stack),
-// which will hold the underlying stackitem.Item.
+// Element represents an element on the stack, technically it's a wrapper around
+// stackitem.Item interface to provide some API simplification for VM.
 type Element struct {
-	value      stackitem.Item
-	next, prev *Element
-	stack      *Stack
+	value stackitem.Item
 }
 
 // NewElement returns a new Element object, with its underlying value inferred
 // to the corresponding type.
-func NewElement(v interface{}) *Element {
-	return &Element{
-		value: stackitem.Make(v),
-	}
-}
-
-// Next returns the next element in the stack.
-func (e *Element) Next() *Element {
-	if elem := e.next; e.stack != nil && elem != &e.stack.top {
-		return elem
-	}
-	return nil
-}
-
-// Prev returns the previous element in the stack.
-func (e *Element) Prev() *Element {
-	if elem := e.prev; e.stack != nil && elem != &e.stack.top {
-		return elem
-	}
-	return nil
+func NewElement(v interface{}) Element {
+	return Element{stackitem.Make(v)}
 }
 
 // Item returns Item contained in the element.
-func (e *Element) Item() stackitem.Item {
+func (e Element) Item() stackitem.Item {
 	return e.value
 }
 
 // Value returns value of the Item contained in the element.
-func (e *Element) Value() interface{} {
+func (e Element) Value() interface{} {
 	return e.value.Value()
 }
 
 // BigInt attempts to get the underlying value of the element as a big integer.
 // Will panic if the assertion failed which will be caught by the VM.
-func (e *Element) BigInt() *big.Int {
+func (e Element) BigInt() *big.Int {
 	val, err := e.value.TryInteger()
 	if err != nil {
 		panic(err)
@@ -82,7 +48,7 @@ func (e *Element) BigInt() *big.Int {
 
 // Bool converts an underlying value of the element to a boolean if it's
 // possible to do so, it will panic otherwise.
-func (e *Element) Bool() bool {
+func (e Element) Bool() bool {
 	b, err := e.value.TryBool()
 	if err != nil {
 		panic(err)
@@ -92,7 +58,7 @@ func (e *Element) Bool() bool {
 
 // Bytes attempts to get the underlying value of the element as a byte array.
 // Will panic if the assertion failed which will be caught by the VM.
-func (e *Element) Bytes() []byte {
+func (e Element) Bytes() []byte {
 	bs, err := e.value.TryBytes()
 	if err != nil {
 		panic(err)
@@ -102,7 +68,7 @@ func (e *Element) Bytes() []byte {
 
 // BytesOrNil attempts to get the underlying value of the element as a byte array or nil.
 // Will panic if the assertion failed which will be caught by the VM.
-func (e *Element) BytesOrNil() []byte {
+func (e Element) BytesOrNil() []byte {
 	if _, ok := e.value.(stackitem.Null); ok {
 		return nil
 	}
@@ -115,7 +81,7 @@ func (e *Element) BytesOrNil() []byte {
 
 // String attempts to get string from the element value.
 // It is assumed to be use in interops and panics if string is not a valid UTF-8 byte sequence.
-func (e *Element) String() string {
+func (e Element) String() string {
 	s, err := stackitem.ToString(e.value)
 	if err != nil {
 		panic(err)
@@ -126,7 +92,7 @@ func (e *Element) String() string {
 // Array attempts to get the underlying value of the element as an array of
 // other items. Will panic if the item type is different which will be caught
 // by the VM.
-func (e *Element) Array() []stackitem.Item {
+func (e Element) Array() []stackitem.Item {
 	switch t := e.value.(type) {
 	case *stackitem.Array:
 		return t.Value().([]stackitem.Item)
@@ -139,7 +105,7 @@ func (e *Element) Array() []stackitem.Item {
 
 // Interop attempts to get the underlying value of the element
 // as an interop item.
-func (e *Element) Interop() *stackitem.Interop {
+func (e Element) Interop() *stackitem.Interop {
 	switch t := e.value.(type) {
 	case *stackitem.Interop:
 		return t
@@ -148,12 +114,11 @@ func (e *Element) Interop() *stackitem.Interop {
 	}
 }
 
-// Stack represents a Stack backed by a double linked list.
+// Stack represents a Stack backed by a slice of Elements.
 type Stack struct {
-	top  Element
-	name string
-	len  int
-	refs *refCounter
+	elems []Element
+	name  string
+	refs  *refCounter
 }
 
 // NewStack returns a new stack name by the given name.
@@ -163,63 +128,46 @@ func NewStack(n string) *Stack {
 
 func newStack(n string, refc *refCounter) *Stack {
 	s := new(Stack)
+	s.elems = make([]Element, 0, 16) // Most of uses are expected to fit into 16 elements.
 	initStack(s, n, refc)
 	return s
 }
 func initStack(s *Stack, n string, refc *refCounter) {
 	s.name = n
 	s.refs = refc
-	s.top.next = &s.top
-	s.top.prev = &s.top
+	s.Clear()
 }
 
 // Clear clears all elements on the stack and set its length to 0.
 func (s *Stack) Clear() {
-	s.top.next = &s.top
-	s.top.prev = &s.top
-	s.len = 0
+	if s.elems != nil {
+		for _, el := range s.elems {
+			s.refs.Remove(el.value)
+		}
+		s.elems = s.elems[:0]
+	}
 }
 
 // Len returns the number of elements that are on the stack.
 func (s *Stack) Len() int {
-	return s.len
-}
-
-// insert inserts the element after element (at) on the stack.
-func (s *Stack) insert(e, at *Element) *Element {
-	// If we insert an element that is already popped from this stack,
-	// we need to clean it up, there are still pointers referencing to it.
-	if e.stack == s {
-		e = NewElement(e.value)
-	}
-
-	n := at.next
-	at.next = e
-	e.prev = at
-	e.next = n
-	n.prev = e
-	e.stack = s
-	s.len++
-
-	s.refs.Add(e.value)
-
-	return e
+	return len(s.elems)
 }
 
 // InsertAt inserts the given item (n) deep on the stack.
-// Be very careful using it and _always_ check both e and n before invocation
-// as it will silently do wrong things otherwise.
-func (s *Stack) InsertAt(e *Element, n int) *Element {
-	before := s.Peek(n - 1)
-	if before == nil {
-		return nil
-	}
-	return s.insert(e, before)
+// Be very careful using it and _always_ check n before invocation
+// as it will panic otherwise.
+func (s *Stack) InsertAt(e Element, n int) {
+	l := len(s.elems)
+	s.elems = append(s.elems, e)
+	copy(s.elems[l-n+1:], s.elems[l-n:l])
+	s.elems[l-n] = e
+	s.refs.Add(e.value)
 }
 
 // Push pushes the given element on the stack.
-func (s *Stack) Push(e *Element) {
-	s.insert(e, &s.top)
+func (s *Stack) Push(e Element) {
+	s.elems = append(s.elems, e)
+	s.refs.Add(e.value)
 }
 
 // PushVal pushes the given value on the stack. It will infer the
@@ -228,63 +176,49 @@ func (s *Stack) PushVal(v interface{}) {
 	s.Push(NewElement(v))
 }
 
-// Pop removes and returns the element on top of the stack.
-func (s *Stack) Pop() *Element {
-	return s.Remove(s.Top())
+// Pop removes and returns the element on top of the stack. Panics if stack is
+// empty.
+func (s *Stack) Pop() Element {
+	l := len(s.elems)
+	e := s.elems[l-1]
+	s.elems = s.elems[:l-1]
+	s.refs.Remove(e.value)
+	return e
 }
 
 // Top returns the element on top of the stack. Nil if the stack
 // is empty.
-func (s *Stack) Top() *Element {
-	if s.len == 0 {
-		return nil
+func (s *Stack) Top() Element {
+	if len(s.elems) == 0 {
+		return Element{}
 	}
-	return s.top.next
+	return s.elems[len(s.elems)-1]
 }
 
 // Back returns the element at the end of the stack. Nil if the stack
 // is empty.
-func (s *Stack) Back() *Element {
-	if s.len == 0 {
-		return nil
+func (s *Stack) Back() Element {
+	if len(s.elems) == 0 {
+		return Element{}
 	}
-	return s.top.prev
+	return s.elems[0]
 }
 
 // Peek returns the element (n) far in the stack beginning from
-// the top of the stack.
-// 	n = 0 => will return the element on top of the stack.
-func (s *Stack) Peek(n int) *Element {
-	i := 0
-	for e := s.Top(); e != nil; e = e.Next() {
-		if n == i {
-			return e
-		}
-		i++
-	}
-	return nil
+// the top of the stack. For n == 0 it's effectively the same as Top,
+// but it'll panic if the stack is empty.
+func (s *Stack) Peek(n int) Element {
+	n = len(s.elems) - n - 1
+	return s.elems[n]
 }
 
 // RemoveAt removes the element (n) deep on the stack beginning
-// from the top of the stack.
-func (s *Stack) RemoveAt(n int) *Element {
-	return s.Remove(s.Peek(n))
-}
-
-// Remove removes and returns the given element from the stack.
-func (s *Stack) Remove(e *Element) *Element {
-	if e == nil {
-		return nil
-	}
-	e.prev.next = e.next
-	e.next.prev = e.prev
-	e.next = nil // avoid memory leaks.
-	e.prev = nil // avoid memory leaks.
-	e.stack = nil
-	s.len--
-
+// from the top of the stack. Panics if called with out of bounds n.
+func (s *Stack) RemoveAt(n int) Element {
+	l := len(s.elems)
+	e := s.elems[l-1-n]
+	s.elems = append(s.elems[:l-1-n], s.elems[l-n:]...)
 	s.refs.Remove(e.value)
-
 	return e
 }
 
@@ -292,15 +226,9 @@ func (s *Stack) Remove(e *Element) *Element {
 // Dup is used for copying elements on to the top of its own stack.
 // 	s.Push(s.Peek(0)) // will result in unexpected behaviour.
 // 	s.Push(s.Dup(0)) // is the correct approach.
-func (s *Stack) Dup(n int) *Element {
+func (s *Stack) Dup(n int) Element {
 	e := s.Peek(n)
-	if e == nil {
-		return nil
-	}
-
-	return &Element{
-		value: e.value.Dup(),
-	}
+	return Element{e.value.Dup()}
 }
 
 // Iter iterates over all the elements int the stack, starting from the top
@@ -308,9 +236,9 @@ func (s *Stack) Dup(n int) *Element {
 // 	s.Iter(func(elem *Element) {
 //		// do something with the element.
 // 	})
-func (s *Stack) Iter(f func(*Element)) {
-	for e := s.Top(); e != nil; e = e.Next() {
-		f(e)
+func (s *Stack) Iter(f func(Element)) {
+	for i := len(s.elems) - 1; i >= 0; i-- {
+		f(s.elems[i])
 	}
 }
 
@@ -319,9 +247,9 @@ func (s *Stack) Iter(f func(*Element)) {
 // 	s.IterBack(func(elem *Element) {
 //		// do something with the element.
 // 	})
-func (s *Stack) IterBack(f func(*Element)) {
-	for e := s.Back(); e != nil; e = e.Prev() {
-		f(e)
+func (s *Stack) IterBack(f func(Element)) {
+	for i := 0; i < len(s.elems); i++ {
+		f(s.elems[i])
 	}
 }
 
@@ -330,37 +258,27 @@ func (s *Stack) Swap(n1, n2 int) error {
 	if n1 < 0 || n2 < 0 {
 		return errors.New("negative index")
 	}
-	if n1 >= s.len || n2 >= s.len {
+	l := len(s.elems)
+	if n1 >= l || n2 >= l {
 		return errors.New("too big index")
 	}
-	if n1 == n2 {
-		return nil
-	}
-	s.swap(n1, n2)
+	s.elems[l-n1-1], s.elems[l-n2-1] = s.elems[l-n2-1], s.elems[l-n1-1]
 	return nil
 }
 
-func (s *Stack) swap(n1, n2 int) {
-	a := s.Peek(n1)
-	b := s.Peek(n2)
-	a.value, b.value = b.value, a.value
-}
-
 // ReverseTop reverses top n items of the stack.
 func (s *Stack) ReverseTop(n int) error {
+	l := len(s.elems)
 	if n < 0 {
 		return errors.New("negative index")
-	} else if n > s.len {
+	} else if n > l {
 		return errors.New("too big index")
 	} else if n <= 1 {
 		return nil
 	}
 
-	a, b := s.Peek(0), s.Peek(n-1)
-	for i := 0; i < n/2; i++ {
-		a.value, b.value = b.value, a.value
-		a = a.Next()
-		b = b.Prev()
+	for i, j := l-n, l-1; i <= j; i, j = i+1, j-1 {
+		s.elems[i], s.elems[j] = s.elems[j], s.elems[i]
 	}
 	return nil
 }
@@ -372,24 +290,16 @@ func (s *Stack) Roll(n int) error {
 	if n < 0 {
 		return errors.New("negative index")
 	}
-	if n >= s.len {
+	l := len(s.elems)
+	if n >= l {
 		return errors.New("too big index")
 	}
 	if n == 0 {
 		return nil
 	}
-	top := s.Peek(0)
-	e := s.Peek(n)
-
-	e.prev.next = e.next
-	e.next.prev = e.prev
-
-	top.prev = e
-	e.next = top
-
-	e.prev = &s.top
-	s.top.next = e
-
+	e := s.elems[l-1-n]
+	copy(s.elems[l-1-n:], s.elems[l-n:])
+	s.elems[l-1] = e
 	return nil
 }
 
@@ -398,10 +308,10 @@ func (s *Stack) Roll(n int) error {
 func (s *Stack) PopSigElements() ([][]byte, error) {
 	var num int
 	var elems [][]byte
-	item := s.Pop()
-	if item == nil {
+	if s.Len() == 0 {
 		return nil, fmt.Errorf("nothing on the stack")
 	}
+	item := s.Pop()
 	switch item.value.(type) {
 	case *stackitem.Array:
 		num = len(item.Array())
@@ -431,8 +341,8 @@ func (s *Stack) PopSigElements() ([][]byte, error) {
 
 // ToArray converts stack to an array of stackitems with top item being the last.
 func (s *Stack) ToArray() []stackitem.Item {
-	items := make([]stackitem.Item, 0, s.len)
-	s.IterBack(func(e *Element) {
+	items := make([]stackitem.Item, 0, len(s.elems))
+	s.IterBack(func(e Element) {
 		items = append(items, e.Item())
 	})
 	return items
diff --git a/pkg/vm/stack_test.go b/pkg/vm/stack_test.go
index 885ccbf08..e596a5eef 100644
--- a/pkg/vm/stack_test.go
+++ b/pkg/vm/stack_test.go
@@ -76,9 +76,6 @@ func TestRemoveAt(t *testing.T) {
 
 	elem := s.RemoveAt(8)
 	assert.Equal(t, elems[1], elem)
-	assert.Nil(t, elem.prev)
-	assert.Nil(t, elem.next)
-	assert.Nil(t, elem.stack)
 
 	// Test if the pointers are moved.
 	assert.Equal(t, elems[0], s.Peek(8))
@@ -147,8 +144,6 @@ func TestRemoveLastElement(t *testing.T) {
 	}
 	elem := s.RemoveAt(1)
 	assert.Equal(t, elems[0], elem)
-	assert.Nil(t, elem.prev)
-	assert.Nil(t, elem.next)
 	assert.Equal(t, 1, s.Len())
 }
 
@@ -163,7 +158,7 @@ func TestIterAfterRemove(t *testing.T) {
 	s.RemoveAt(0)
 
 	i := 0
-	s.Iter(func(elem *Element) {
+	s.Iter(func(_ Element) {
 		i++
 	})
 	assert.Equal(t, len(elems)-1, i)
@@ -180,15 +175,16 @@ func TestIteration(t *testing.T) {
 	}
 	assert.Equal(t, len(elems), s.Len())
 
-	iteratedElems := make([]*Element, 0)
+	iteratedElems := make([]Element, 0)
 
-	s.Iter(func(elem *Element) {
+	s.Iter(func(elem Element) {
 		iteratedElems = append(iteratedElems, elem)
 	})
+
 	// Top to bottom order of iteration.
-	poppedElems := make([]*Element, 0)
-	for elem := s.Pop(); elem != nil; elem = s.Pop() {
-		poppedElems = append(poppedElems, elem)
+	poppedElems := make([]Element, 0)
+	for s.Len() != 0 {
+		poppedElems = append(poppedElems, s.Pop())
 	}
 	assert.Equal(t, poppedElems, iteratedElems)
 }
@@ -204,9 +200,9 @@ func TestBackIteration(t *testing.T) {
 	}
 	assert.Equal(t, len(elems), s.Len())
 
-	iteratedElems := make([]*Element, 0)
+	iteratedElems := make([]Element, 0)
 
-	s.IterBack(func(elem *Element) {
+	s.IterBack(func(elem Element) {
 		iteratedElems = append(iteratedElems, elem)
 	})
 	// Bottom to the top order of iteration.
@@ -331,6 +327,25 @@ func TestRoll(t *testing.T) {
 	assert.Equal(t, int64(1), s.Pop().BigInt().Int64())
 }
 
+func TestInsertAt(t *testing.T) {
+	s := NewStack("stack")
+	s.PushVal(1)
+	s.PushVal(2)
+	s.PushVal(3)
+	s.PushVal(4)
+	s.PushVal(5)
+
+	e := s.Dup(1) // it's `4`
+	s.InsertAt(e, 3)
+
+	assert.Equal(t, int64(5), s.Peek(0).BigInt().Int64())
+	assert.Equal(t, int64(4), s.Peek(1).BigInt().Int64())
+	assert.Equal(t, int64(3), s.Peek(2).BigInt().Int64())
+	assert.Equal(t, int64(4), s.Peek(3).BigInt().Int64())
+	assert.Equal(t, int64(2), s.Peek(4).BigInt().Int64())
+	assert.Equal(t, int64(1), s.Peek(5).BigInt().Int64())
+}
+
 func TestPopSigElements(t *testing.T) {
 	s := NewStack("test")
 
@@ -369,8 +384,8 @@ func TestPopSigElements(t *testing.T) {
 	assert.Equal(t, z, [][]byte{b1, b2})
 }
 
-func makeElements(n int) []*Element {
-	elems := make([]*Element, n)
+func makeElements(n int) []Element {
+	elems := make([]Element, n)
 	for i := 0; i < n; i++ {
 		elems[i] = NewElement(i)
 	}
diff --git a/pkg/vm/stackitem/serialization.go b/pkg/vm/stackitem/serialization.go
index 8db760e37..bda4fbc80 100644
--- a/pkg/vm/stackitem/serialization.go
+++ b/pkg/vm/stackitem/serialization.go
@@ -95,14 +95,12 @@ func (w *serContext) serialize(item Item) error {
 	switch t := item.(type) {
 	case *ByteArray:
 		w.data = append(w.data, byte(ByteArrayT))
-		data := t.Value().([]byte)
-		w.appendVarUint(uint64(len(data)))
-		w.data = append(w.data, data...)
+		w.appendVarUint(uint64(len(*t)))
+		w.data = append(w.data, *t...)
 	case *Buffer:
 		w.data = append(w.data, byte(BufferT))
-		data := t.Value().([]byte)
-		w.appendVarUint(uint64(len(data)))
-		w.data = append(w.data, data...)
+		w.appendVarUint(uint64(len(*t)))
+		w.data = append(w.data, *t...)
 	case Bool:
 		w.data = append(w.data, byte(BooleanT))
 		if t {
@@ -112,10 +110,9 @@ func (w *serContext) serialize(item Item) error {
 		}
 	case *BigInteger:
 		w.data = append(w.data, byte(IntegerT))
-		v := t.Value().(*big.Int)
 		ln := len(w.data)
 		w.data = append(w.data, 0)
-		data := bigint.ToPreallocatedBytes(v, w.data[len(w.data):])
+		data := bigint.ToPreallocatedBytes((*big.Int)(t), w.data[len(w.data):])
 		w.data[ln] = byte(len(data))
 		w.data = append(w.data, data...)
 	case *Interop:
diff --git a/pkg/vm/stackitem/serialization_test.go b/pkg/vm/stackitem/serialization_test.go
index 139fbcbcf..9b496dd52 100644
--- a/pkg/vm/stackitem/serialization_test.go
+++ b/pkg/vm/stackitem/serialization_test.go
@@ -207,3 +207,15 @@ func BenchmarkEncodeBinary(b *testing.B) {
 		}
 	}
 }
+
+func BenchmarkSerializeSimple(b *testing.B) {
+	s := NewStruct(nil)
+	s.Append(Make(100500))
+	s.Append(Make("1aada0032aba1ef6d1f0")) // Mimicking uint160.
+	for i := 0; i < b.N; i++ {
+		_, err := Serialize(s)
+		if err != nil {
+			b.FailNow()
+		}
+	}
+}
diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go
index 2f973ed25..35b89dfa3 100644
--- a/pkg/vm/vm.go
+++ b/pkg/vm/vm.go
@@ -282,7 +282,7 @@ func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) {
 	ctx := NewContextWithParams(b, 0, -1, 0)
 	v.estack = newStack("evaluation", &v.refs)
 	ctx.estack = v.estack
-	ctx.tryStack = newStack("exception", nil)
+	initStack(&ctx.tryStack, "exception", nil)
 	ctx.callFlag = f
 	ctx.static = newSlot(&v.refs)
 	ctx.callingScriptHash = v.GetCurrentScriptHash()
@@ -328,11 +328,10 @@ func (v *VM) Context() *Context {
 // PopResult is used to pop the first item of the evaluation stack. This allows
 // us to test compiler and vm in a bi-directional way.
 func (v *VM) PopResult() interface{} {
-	e := v.estack.Pop()
-	if e != nil {
-		return e.Value()
+	if v.estack.Len() == 0 {
+		return nil
 	}
-	return nil
+	return v.estack.Pop().Value()
 }
 
 // Stack returns json formatted representation of the given stack.
@@ -448,8 +447,8 @@ func (v *VM) StepOut() error {
 		v.state = NoneState
 	}
 
-	expSize := v.istack.len
-	for v.state == NoneState && v.istack.len >= expSize {
+	expSize := v.istack.Len()
+	for v.state == NoneState && v.istack.Len() >= expSize {
 		err = v.StepInto()
 	}
 	if v.state == NoneState {
@@ -470,10 +469,10 @@ func (v *VM) StepOver() error {
 		v.state = NoneState
 	}
 
-	expSize := v.istack.len
+	expSize := v.istack.Len()
 	for {
 		err = v.StepInto()
-		if !(v.state == NoneState && v.istack.len > expSize) {
+		if !(v.state == NoneState && v.istack.Len() > expSize) {
 			break
 		}
 	}
@@ -739,20 +738,20 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
 		v.estack.Pop()
 
 	case opcode.NIP:
-		elem := v.estack.RemoveAt(1)
-		if elem == nil {
+		if v.estack.Len() < 2 {
 			panic("no second element found")
 		}
+		_ = v.estack.RemoveAt(1)
 
 	case opcode.XDROP:
 		n := int(v.estack.Pop().BigInt().Int64())
 		if n < 0 {
 			panic("invalid length")
 		}
-		e := v.estack.RemoveAt(n)
-		if e == nil {
+		if v.estack.Len() < n+1 {
 			panic("bad index")
 		}
+		_ = v.estack.RemoveAt(n)
 
 	case opcode.CLEAR:
 		v.estack.Clear()
@@ -761,10 +760,10 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
 		v.estack.Push(v.estack.Dup(0))
 
 	case opcode.OVER:
-		a := v.estack.Dup(1)
-		if a == nil {
+		if v.estack.Len() < 2 {
 			panic("no second element found")
 		}
+		a := v.estack.Dup(1)
 		v.estack.Push(a)
 
 	case opcode.PICK:
@@ -772,20 +771,17 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
 		if n < 0 {
 			panic("negative stack item returned")
 		}
-		a := v.estack.Dup(n)
-		if a == nil {
+		if v.estack.Len() < n+1 {
 			panic("no nth element found")
 		}
+		a := v.estack.Dup(n)
 		v.estack.Push(a)
 
 	case opcode.TUCK:
-		a := v.estack.Dup(0)
-		if a == nil {
-			panic("no top-level element found")
-		}
 		if v.estack.Len() < 2 {
-			panic("can't TUCK with a one-element stack")
+			panic("too short stack to TUCK")
 		}
+		a := v.estack.Dup(0)
 		v.estack.InsertAt(a, 2)
 
 	case opcode.SWAP:
@@ -821,10 +817,8 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
 
 	// Bit operations.
 	case opcode.INVERT:
-		// inplace
-		e := v.estack.Peek(0)
-		i := e.BigInt()
-		e.value = stackitem.Make(new(big.Int).Not(i))
+		i := v.estack.Pop().BigInt()
+		v.estack.PushVal(new(big.Int).Not(i))
 
 	case opcode.AND:
 		b := v.estack.Pop().BigInt()
@@ -842,14 +836,11 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
 		v.estack.PushVal(new(big.Int).Xor(b, a))
 
 	case opcode.EQUAL, opcode.NOTEQUAL:
+		if v.estack.Len() < 2 {
+			panic("need a pair of elements on the stack")
+		}
 		b := v.estack.Pop()
-		if b == nil {
-			panic("no top-level element found")
-		}
 		a := v.estack.Pop()
-		if a == nil {
-			panic("no second-to-the-top element found")
-		}
 		v.estack.PushVal(a.value.Equals(b.value) == (op == opcode.EQUAL))
 
 	// Numeric operations.
@@ -1100,7 +1091,7 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
 			if index < 0 {
 				panic("invalid key")
 			}
-			v.estack.Push(&Element{value: t.Value().([]stackitem.MapElement)[index].Value.Dup()})
+			v.estack.Push(Element{value: t.Value().([]stackitem.MapElement)[index].Value.Dup()})
 		default:
 			arr := obj.Bytes()
 			if index < 0 || index >= len(arr) {
@@ -1318,13 +1309,13 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
 		}
 
 	case opcode.NEWMAP:
-		v.estack.Push(&Element{value: stackitem.NewMap()})
+		v.estack.Push(Element{value: stackitem.NewMap()})
 
 	case opcode.KEYS:
-		item := v.estack.Pop()
-		if item == nil {
+		if v.estack.Len() == 0 {
 			panic("no argument")
 		}
+		item := v.estack.Pop()
 
 		m, ok := item.value.(*stackitem.Map)
 		if !ok {
@@ -1338,10 +1329,10 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
 		v.estack.PushVal(arr)
 
 	case opcode.VALUES:
-		item := v.estack.Pop()
-		if item == nil {
+		if v.estack.Len() == 0 {
 			panic("no argument")
 		}
+		item := v.estack.Pop()
 
 		var arr []stackitem.Item
 		switch t := item.value.(type) {
@@ -1363,13 +1354,13 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro
 		v.estack.PushVal(arr)
 
 	case opcode.HASKEY:
+		if v.estack.Len() < 2 {
+			panic("not enough arguments")
+		}
 		key := v.estack.Pop()
 		validateMapKey(key)
 
 		c := v.estack.Pop()
-		if c == nil {
-			panic("no value found")
-		}
 		switch t := c.value.(type) {
 		case *stackitem.Array, *stackitem.Struct:
 			index := key.BigInt().Int64()
@@ -1519,7 +1510,7 @@ func (v *VM) call(ctx *Context, offset int) {
 	newCtx.RetCount = -1
 	newCtx.local = nil
 	newCtx.arguments = nil
-	newCtx.tryStack = newStack("exception", nil)
+	initStack(&newCtx.tryStack, "exception", nil)
 	newCtx.NEF = ctx.NEF
 	v.istack.PushVal(newCtx)
 	v.Jump(newCtx, offset)
@@ -1559,16 +1550,15 @@ func calcJumpOffset(ctx *Context, parameter []byte) (int, int, error) {
 }
 
 func (v *VM) handleException() {
-	pop := 0
-	ictxv := v.istack.Peek(0)
-	ictx := ictxv.Value().(*Context)
-	for ictx != nil {
-		e := ictx.tryStack.Peek(0)
-		for e != nil {
+	for pop := 0; pop < v.istack.Len(); pop++ {
+		ictxv := v.istack.Peek(pop)
+		ictx := ictxv.Value().(*Context)
+		for j := 0; j < ictx.tryStack.Len(); j++ {
+			e := ictx.tryStack.Peek(j)
 			ectx := e.Value().(*exceptionHandlingContext)
 			if ectx.State == eFinally || (ectx.State == eCatch && !ectx.HasFinally()) {
 				ictx.tryStack.Pop()
-				e = ictx.tryStack.Peek(0)
+				j = -1
 				continue
 			}
 			for i := 0; i < pop; i++ {
@@ -1586,12 +1576,6 @@ func (v *VM) handleException() {
 			}
 			return
 		}
-		pop++
-		ictxv = ictxv.Next()
-		if ictxv == nil {
-			break
-		}
-		ictx = ictxv.Value().(*Context)
 	}
 	throwUnhandledException(v.uncaughtException)
 }
@@ -1753,17 +1737,18 @@ func makeArrayOfType(n int, typ stackitem.Type) []stackitem.Item {
 	return items
 }
 
-func validateMapKey(key *Element) {
-	if key == nil {
+func validateMapKey(key Element) {
+	item := key.Item()
+	if item == nil {
 		panic("no key found")
 	}
-	if err := stackitem.IsValidMapKey(key.Item()); err != nil {
+	if err := stackitem.IsValidMapKey(item); err != nil {
 		panic(err)
 	}
 }
 
 func (v *VM) checkInvocationStackSize() {
-	if v.istack.len >= MaxInvocationStackSize {
+	if v.istack.Len() >= MaxInvocationStackSize {
 		panic("invocation stack is too big")
 	}
 }
@@ -1785,7 +1770,7 @@ func (v *VM) GetCallingScriptHash() util.Uint160 {
 
 // GetEntryScriptHash implements ScriptHashGetter interface.
 func (v *VM) GetEntryScriptHash() util.Uint160 {
-	return v.getContextScriptHash(v.istack.len - 1)
+	return v.getContextScriptHash(v.istack.Len() - 1)
 }
 
 // GetCurrentScriptHash implements ScriptHashGetter interface.
diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go
index 3391ddeca..63ca28fd1 100644
--- a/pkg/vm/vm_test.go
+++ b/pkg/vm/vm_test.go
@@ -1176,7 +1176,7 @@ func TestPICKITEMDupMap(t *testing.T) {
 	vm := load(prog)
 	m := stackitem.NewMap()
 	m.Add(stackitem.Make(42), stackitem.Make(-1))
-	vm.estack.Push(&Element{value: m})
+	vm.estack.Push(Element{value: m})
 	runVM(t, vm)
 	assert.Equal(t, 2, vm.estack.Len())
 	assert.Equal(t, int64(1), vm.estack.Pop().BigInt().Int64())
@@ -1245,7 +1245,7 @@ func TestSETITEMBigMapGood(t *testing.T) {
 	for i := 0; i < MaxStackSize-3; i++ {
 		m.Add(stackitem.Make(i), stackitem.Make(i))
 	}
-	vm.estack.Push(&Element{value: m})
+	vm.estack.Push(Element{value: m})
 	vm.estack.PushVal(0)
 	vm.estack.PushVal(0)
 
@@ -1274,7 +1274,7 @@ func TestKEYSMap(t *testing.T) {
 	m := stackitem.NewMap()
 	m.Add(stackitem.Make(5), stackitem.Make(6))
 	m.Add(stackitem.Make([]byte{0, 1}), stackitem.Make(6))
-	vm.estack.Push(&Element{value: m})
+	vm.estack.Push(Element{value: m})
 
 	runVM(t, vm)
 	assert.Equal(t, 1, vm.estack.Len())
@@ -1298,7 +1298,7 @@ func TestVALUESMap(t *testing.T) {
 	m := stackitem.NewMap()
 	m.Add(stackitem.Make(5), stackitem.Make([]byte{2, 3}))
 	m.Add(stackitem.Make([]byte{0, 1}), stackitem.Make([]stackitem.Item{}))
-	vm.estack.Push(&Element{value: m})
+	vm.estack.Push(Element{value: m})
 
 	runVM(t, vm)
 	assert.Equal(t, 1, vm.estack.Len())
@@ -1880,7 +1880,7 @@ func TestREVERSEITEMSGoodStruct(t *testing.T) {
 		for i := range elements {
 			arr[i] = stackitem.Make(elements[i])
 		}
-		vm.estack.Push(&Element{value: stackitem.NewStruct(arr)})
+		vm.estack.Push(Element{value: stackitem.NewStruct(arr)})
 
 		runVM(t, vm)
 		assert.Equal(t, 2, vm.estack.Len())
@@ -1944,8 +1944,8 @@ func TestREMOVEMap(t *testing.T) {
 	m := stackitem.NewMap()
 	m.Add(stackitem.Make(5), stackitem.Make(3))
 	m.Add(stackitem.Make([]byte{0, 1}), stackitem.Make([]byte{2, 3}))
-	vm.estack.Push(&Element{value: m})
-	vm.estack.Push(&Element{value: m})
+	vm.estack.Push(Element{value: m})
+	vm.estack.Push(Element{value: m})
 	vm.estack.PushVal(stackitem.Make(5))
 
 	runVM(t, vm)