diff --git a/py/bc.c b/py/bc.c
index 91ef9c070fde6fb586704a04847c1129940b1e3e..b5515bcd7916136ffa7c1b666a9faf7a19334843 100644
--- a/py/bc.c
+++ b/py/bc.c
@@ -93,7 +93,6 @@ void mp_setup_code_state(mp_code_state *code_state, mp_obj_t self_in, mp_uint_t
     // usage for the common case of positional only args.
     mp_obj_fun_bc_t *self = self_in;
     mp_uint_t n_state = code_state->n_state;
-    const byte *ip = code_state->ip;
 
     code_state->code_info = self->bytecode;
     code_state->sp = &code_state->state[0] - 1;
@@ -153,13 +152,21 @@ void mp_setup_code_state(mp_code_state *code_state, mp_obj_t self_in, mp_uint_t
             *var_pos_kw_args = dict;
         }
 
+        // get pointer to arg_names array at start of bytecode prelude
+        const mp_obj_t *arg_names;
+        {
+            const byte *code_info = code_state->code_info;
+            mp_uint_t code_info_size = mp_decode_uint(&code_info);
+            arg_names = (const mp_obj_t*)(code_state->code_info + code_info_size);
+        }
+
         for (mp_uint_t i = 0; i < n_kw; i++) {
-            qstr arg_name = MP_OBJ_QSTR_VALUE(kwargs[2 * i]);
+            mp_obj_t wanted_arg_name = kwargs[2 * i];
             for (mp_uint_t j = 0; j < self->n_pos_args + self->n_kwonly_args; j++) {
-                if (arg_name == self->args[j]) {
+                if (wanted_arg_name == arg_names[j]) {
                     if (code_state->state[n_state - 1 - j] != MP_OBJ_NULL) {
                         nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_TypeError,
-                            "function got multiple values for argument '%s'", qstr_str(arg_name)));
+                            "function got multiple values for argument '%s'", qstr_str(MP_OBJ_QSTR_VALUE(wanted_arg_name))));
                     }
                     code_state->state[n_state - 1 - j] = kwargs[2 * i + 1];
                     goto continue2;
@@ -202,13 +209,13 @@ continue2:;
             if (code_state->state[n_state - 1 - self->n_pos_args - i] == MP_OBJ_NULL) {
                 mp_map_elem_t *elem = NULL;
                 if (self->has_def_kw_args) {
-                    elem = mp_map_lookup(&((mp_obj_dict_t*)self->extra_args[self->n_def_args])->map, MP_OBJ_NEW_QSTR(self->args[self->n_pos_args + i]), MP_MAP_LOOKUP);
+                    elem = mp_map_lookup(&((mp_obj_dict_t*)self->extra_args[self->n_def_args])->map, arg_names[self->n_pos_args + i], MP_MAP_LOOKUP);
                 }
                 if (elem != NULL) {
                     code_state->state[n_state - 1 - self->n_pos_args - i] = elem->value;
                 } else {
                     nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_TypeError,
-                        "function missing required keyword argument '%s'", qstr_str(self->args[self->n_pos_args + i])));
+                        "function missing required keyword argument '%s'", qstr_str(MP_OBJ_QSTR_VALUE(arg_names[self->n_pos_args + i]))));
                 }
             }
         }
@@ -225,6 +232,7 @@ continue2:;
     }
 
     // bytecode prelude: initialise closed over variables
+    const byte *ip = code_state->ip;
     for (mp_uint_t n_local = *ip++; n_local > 0; n_local--) {
         mp_uint_t local_num = *ip++;
         code_state->state[n_state - 1 - local_num] = mp_obj_new_cell(code_state->state[n_state - 1 - local_num]);
diff --git a/py/bc.h b/py/bc.h
index 6cda15b0461d4fb729becb6167ed6b6ffedb7cb0..058eaba401dc98a7c053883de55611753ad2951e 100644
--- a/py/bc.h
+++ b/py/bc.h
@@ -53,7 +53,7 @@ mp_uint_t mp_decode_uint(const byte **ptr);
 
 mp_vm_return_kind_t mp_execute_bytecode(mp_code_state *code_state, volatile mp_obj_t inject_exc);
 void mp_setup_code_state(mp_code_state *code_state, mp_obj_t self_in, mp_uint_t n_args, mp_uint_t n_kw, const mp_obj_t *args);
-void mp_bytecode_print(const void *descr, const byte *code, mp_uint_t len);
+void mp_bytecode_print(const void *descr, mp_uint_t n_total_args, const byte *code, mp_uint_t len);
 void mp_bytecode_print2(const byte *code, mp_uint_t len);
 
 // Helper macros to access pointer with least significant bit holding a flag
diff --git a/py/emitbc.c b/py/emitbc.c
index 05dc79ef4fc84246862f9176b6b11666abced9f1..cee6d3396c1cf0e1075873152df5befdc1ea691a 100644
--- a/py/emitbc.c
+++ b/py/emitbc.c
@@ -218,6 +218,13 @@ STATIC void emit_write_bytecode_byte_uint(emit_t* emit, byte b, mp_uint_t val) {
     emit_write_uint(emit, emit_get_cur_to_write_bytecode, val);
 }
 
+STATIC void emit_write_bytecode_prealigned_ptr(emit_t* emit, void *ptr) {
+    mp_uint_t *c = (mp_uint_t*)emit_get_cur_to_write_bytecode(emit, sizeof(mp_uint_t));
+    // Verify thar c is already uint-aligned
+    assert(c == MP_ALIGN(c, sizeof(mp_uint_t)));
+    *c = (mp_uint_t)ptr;
+}
+
 // aligns the pointer so it is friendly to GC
 STATIC void emit_write_bytecode_byte_ptr(emit_t* emit, byte b, void *ptr) {
     emit_write_bytecode_byte(emit, b);
@@ -294,7 +301,16 @@ STATIC void emit_bc_start_pass(emit_t *emit, pass_kind_t pass, scope_t *scope) {
     emit_write_code_info_qstr(emit, scope->simple_name);
     emit_write_code_info_qstr(emit, scope->source_file);
 
-    // bytecode prelude: local state size and exception stack size; 16 bit uints for now
+    // bytecode prelude: argument names (needed to resolve positional args passed as keywords)
+    // we store them as full word-sized objects for efficient access in mp_setup_code_state
+    // this is the start of the prelude and is guaranteed to be aligned on a word boundary
+    {
+        for (int i = 0; i < scope->num_pos_args + scope->num_kwonly_args; i++) {
+            emit_write_bytecode_prealigned_ptr(emit, MP_OBJ_NEW_QSTR(scope->id_info[i].qst));
+        }
+    }
+
+    // bytecode prelude: local state size and exception stack size
     {
         mp_uint_t n_state = scope->num_locals + scope->stack_size;
         if (n_state == 0) {
@@ -358,13 +374,9 @@ STATIC void emit_bc_end_pass(emit_t *emit) {
         emit->code_base = m_new0(byte, emit->code_info_size + emit->bytecode_size);
 
     } else if (emit->pass == MP_PASS_EMIT) {
-        qstr *arg_names = m_new(qstr, emit->scope->num_pos_args + emit->scope->num_kwonly_args);
-        for (int i = 0; i < emit->scope->num_pos_args + emit->scope->num_kwonly_args; i++) {
-            arg_names[i] = emit->scope->id_info[i].qst;
-        }
         mp_emit_glue_assign_bytecode(emit->scope->raw_code, emit->code_base,
             emit->code_info_size + emit->bytecode_size,
-            emit->scope->num_pos_args, emit->scope->num_kwonly_args, arg_names,
+            emit->scope->num_pos_args, emit->scope->num_kwonly_args,
             emit->scope->scope_flags);
     }
 }
diff --git a/py/emitglue.c b/py/emitglue.c
index dd31de925b3e2353867c5c6b28b2f0c405fb1793..41a6720e7092b771f0b88dbadd451cc00854bfa5 100644
--- a/py/emitglue.c
+++ b/py/emitglue.c
@@ -55,26 +55,20 @@ mp_raw_code_t *mp_emit_glue_new_raw_code(void) {
     return rc;
 }
 
-void mp_emit_glue_assign_bytecode(mp_raw_code_t *rc, byte *code, mp_uint_t len, mp_uint_t n_pos_args, mp_uint_t n_kwonly_args, qstr *arg_names, mp_uint_t scope_flags) {
+void mp_emit_glue_assign_bytecode(mp_raw_code_t *rc, byte *code, mp_uint_t len, mp_uint_t n_pos_args, mp_uint_t n_kwonly_args, mp_uint_t scope_flags) {
     rc->kind = MP_CODE_BYTECODE;
     rc->scope_flags = scope_flags;
     rc->n_pos_args = n_pos_args;
     rc->n_kwonly_args = n_kwonly_args;
-    rc->arg_names = arg_names;
     rc->u_byte.code = code;
     rc->u_byte.len = len;
 
 #ifdef DEBUG_PRINT
     DEBUG_printf("assign byte code: code=%p len=" UINT_FMT " n_pos_args=" UINT_FMT " n_kwonly_args=" UINT_FMT " flags=%x\n", code, len, n_pos_args, n_kwonly_args, (uint)scope_flags);
-    DEBUG_printf("  arg names:");
-    for (int i = 0; i < n_pos_args + n_kwonly_args; i++) {
-        DEBUG_printf(" %s", qstr_str(arg_names[i]));
-    }
-    DEBUG_printf("\n");
 #endif
 #if MICROPY_DEBUG_PRINTERS
     if (mp_verbose_flag > 0) {
-        mp_bytecode_print(rc, code, len);
+        mp_bytecode_print(rc, n_pos_args + n_kwonly_args, code, len);
     }
 #endif
 }
@@ -121,7 +115,7 @@ mp_obj_t mp_make_function_from_raw_code(mp_raw_code_t *rc, mp_obj_t def_args, mp
     mp_obj_t fun;
     switch (rc->kind) {
         case MP_CODE_BYTECODE:
-            fun = mp_obj_new_fun_bc(rc->scope_flags, rc->arg_names, rc->n_pos_args, rc->n_kwonly_args, def_args, def_kw_args, rc->u_byte.code);
+            fun = mp_obj_new_fun_bc(rc->scope_flags, rc->n_pos_args, rc->n_kwonly_args, def_args, def_kw_args, rc->u_byte.code);
             break;
         #if MICROPY_EMIT_NATIVE
         case MP_CODE_NATIVE_PY:
diff --git a/py/emitglue.h b/py/emitglue.h
index 91d5285ad5e13604c9a368d30847247efdda7beb..fd55af26a6dff651f1cf487f9584de2fc1c09733 100644
--- a/py/emitglue.h
+++ b/py/emitglue.h
@@ -40,7 +40,6 @@ typedef struct _mp_raw_code_t {
     mp_uint_t scope_flags : 7;
     mp_uint_t n_pos_args : 11;
     mp_uint_t n_kwonly_args : 11;
-    qstr *arg_names;
     union {
         struct {
             byte *code;
@@ -55,7 +54,7 @@ typedef struct _mp_raw_code_t {
 
 mp_raw_code_t *mp_emit_glue_new_raw_code(void);
 
-void mp_emit_glue_assign_bytecode(mp_raw_code_t *rc, byte *code, mp_uint_t len, mp_uint_t n_pos_args, mp_uint_t n_kwonly_args, qstr *arg_names, mp_uint_t scope_flags);
+void mp_emit_glue_assign_bytecode(mp_raw_code_t *rc, byte *code, mp_uint_t len, mp_uint_t n_pos_args, mp_uint_t n_kwonly_args, mp_uint_t scope_flags);
 void mp_emit_glue_assign_native(mp_raw_code_t *rc, mp_raw_code_kind_t kind, void *fun_data, mp_uint_t fun_len, mp_uint_t n_args, mp_uint_t type_sig);
 
 mp_obj_t mp_make_function_from_raw_code(mp_raw_code_t *rc, mp_obj_t def_args, mp_obj_t def_kw_args);
diff --git a/py/obj.h b/py/obj.h
index 0ae9dc0303c944ca14c2d169fe53cb912f6f136e..4d4c73a00196587bc60f0b290344f82ec32cb66b 100644
--- a/py/obj.h
+++ b/py/obj.h
@@ -383,7 +383,7 @@ mp_obj_t mp_obj_new_exception_arg1(const mp_obj_type_t *exc_type, mp_obj_t arg);
 mp_obj_t mp_obj_new_exception_args(const mp_obj_type_t *exc_type, mp_uint_t n_args, const mp_obj_t *args);
 mp_obj_t mp_obj_new_exception_msg(const mp_obj_type_t *exc_type, const char *msg);
 mp_obj_t mp_obj_new_exception_msg_varg(const mp_obj_type_t *exc_type, const char *fmt, ...); // counts args by number of % symbols in fmt, excluding %%; can only handle void* sizes (ie no float/double!)
-mp_obj_t mp_obj_new_fun_bc(mp_uint_t scope_flags, qstr *args, mp_uint_t n_pos_args, mp_uint_t n_kwonly_args, mp_obj_t def_args, mp_obj_t def_kw_args, const byte *code);
+mp_obj_t mp_obj_new_fun_bc(mp_uint_t scope_flags, mp_uint_t n_pos_args, mp_uint_t n_kwonly_args, mp_obj_t def_args, mp_obj_t def_kw_args, const byte *code);
 mp_obj_t mp_obj_new_fun_native(mp_uint_t n_args, void *fun_data);
 mp_obj_t mp_obj_new_fun_viper(mp_uint_t n_args, void *fun_data, mp_uint_t type_sig);
 mp_obj_t mp_obj_new_fun_asm(mp_uint_t n_args, void *fun_data);
diff --git a/py/objfun.c b/py/objfun.c
index e64d088bfa9091b9583aac12ffc0efaf5dc3e5fd..3adf05e3b0e410e9ff3fdbab6b1cb80859846477 100644
--- a/py/objfun.c
+++ b/py/objfun.c
@@ -173,7 +173,10 @@ STATIC mp_obj_t fun_bc_call(mp_obj_t self_in, mp_uint_t n_args, mp_uint_t n_kw,
     mp_uint_t code_info_size = mp_decode_uint(&code_info);
     const byte *ip = self->bytecode + code_info_size;
 
-    // bytecode prelude: state size and exception stack size; 16 bit uints
+    // bytecode prelude: skip arg names
+    ip += (self->n_pos_args + self->n_kwonly_args) * sizeof(mp_obj_t);
+
+    // bytecode prelude: state size and exception stack size
     mp_uint_t n_state = mp_decode_uint(&ip);
     mp_uint_t n_exc_stack = mp_decode_uint(&ip);
 
@@ -268,7 +271,7 @@ const mp_obj_type_t mp_type_fun_bc = {
     .binary_op = mp_obj_fun_binary_op,
 };
 
-mp_obj_t mp_obj_new_fun_bc(mp_uint_t scope_flags, qstr *args, mp_uint_t n_pos_args, mp_uint_t n_kwonly_args, mp_obj_t def_args_in, mp_obj_t def_kw_args, const byte *code) {
+mp_obj_t mp_obj_new_fun_bc(mp_uint_t scope_flags, mp_uint_t n_pos_args, mp_uint_t n_kwonly_args, mp_obj_t def_args_in, mp_obj_t def_kw_args, const byte *code) {
     mp_uint_t n_def_args = 0;
     mp_uint_t n_extra_args = 0;
     mp_obj_tuple_t *def_args = def_args_in;
@@ -283,7 +286,6 @@ mp_obj_t mp_obj_new_fun_bc(mp_uint_t scope_flags, qstr *args, mp_uint_t n_pos_ar
     mp_obj_fun_bc_t *o = m_new_obj_var(mp_obj_fun_bc_t, mp_obj_t, n_extra_args);
     o->base.type = &mp_type_fun_bc;
     o->globals = mp_globals_get();
-    o->args = args;
     o->n_pos_args = n_pos_args;
     o->n_kwonly_args = n_kwonly_args;
     o->n_def_args = n_def_args;
diff --git a/py/objfun.h b/py/objfun.h
index cc8035ef37389c54e132ff2167eebe40fefbdb82..e23fe8d529db3af58c13af77fa719ce4c1cf84f3 100644
--- a/py/objfun.h
+++ b/py/objfun.h
@@ -27,14 +27,13 @@
 typedef struct _mp_obj_fun_bc_t {
     mp_obj_base_t base;
     mp_obj_dict_t *globals;         // the context within which this function was defined
-    mp_uint_t n_pos_args : 16;      // number of arguments this function takes
-    mp_uint_t n_kwonly_args : 16;   // number of arguments this function takes
-    mp_uint_t n_def_args : 16;      // number of default arguments
+    mp_uint_t n_pos_args : 8;       // number of arguments this function takes
+    mp_uint_t n_kwonly_args : 8;    // number of keyword-only arguments this function takes
+    mp_uint_t n_def_args : 8;       // number of default arguments
     mp_uint_t has_def_kw_args : 1;  // set if this function has default keyword args
     mp_uint_t takes_var_args : 1;   // set if this function takes variable args
     mp_uint_t takes_kw_args : 1;    // set if this function takes keyword args
     const byte *bytecode;           // bytecode for the function
-    qstr *args;                     // argument names (needed to resolve positional args passed as keywords)
     // the following extra_args array is allocated space to take (in order):
     //  - values of positional default args (if any)
     //  - a single slot for default kw args dict (if it has them)
diff --git a/py/objgenerator.c b/py/objgenerator.c
index 9d16d2bee06f172b5475e4b5ad5e9233267ef446..ce26cbce26523f1dd3f5c69e5eaa398d50134880 100644
--- a/py/objgenerator.c
+++ b/py/objgenerator.c
@@ -62,6 +62,9 @@ STATIC mp_obj_t gen_wrap_call(mp_obj_t self_in, mp_uint_t n_args, mp_uint_t n_kw
     mp_uint_t code_info_size = mp_decode_uint(&code_info);
     const byte *ip = self_fun->bytecode + code_info_size;
 
+    // bytecode prelude: skip arg names
+    ip += (self_fun->n_pos_args + self_fun->n_kwonly_args) * sizeof(mp_obj_t);
+
     // bytecode prelude: get state size and exception stack size
     mp_uint_t n_state = mp_decode_uint(&ip);
     mp_uint_t n_exc_stack = mp_decode_uint(&ip);
diff --git a/py/showbc.c b/py/showbc.c
index 28fed14c9ebfa0d97d4ca40d8dbd42e834e166ce..13d257d304a06add44a3efcd1f60cb8e4e49b6ac 100644
--- a/py/showbc.c
+++ b/py/showbc.c
@@ -57,7 +57,7 @@
     ip += sizeof(mp_uint_t); \
 } while (0)
 
-void mp_bytecode_print(const void *descr, const byte *ip, mp_uint_t len) {
+void mp_bytecode_print(const void *descr, mp_uint_t n_total_args, const byte *ip, mp_uint_t len) {
     const byte *ip_start = ip;
 
     // get code info size
@@ -80,6 +80,14 @@ void mp_bytecode_print(const void *descr, const byte *ip, mp_uint_t len) {
     }
     printf("\n");
 
+    // bytecode prelude: arg names (as qstr objects)
+    printf("arg names:");
+    for (int i = 0; i < n_total_args; i++) {
+        printf(" %s", qstr_str(MP_OBJ_QSTR_VALUE(*(mp_obj_t*)ip)));
+        ip += sizeof(mp_obj_t);
+    }
+    printf("\n");
+
     // bytecode prelude: state size and exception stack size; 16 bit uints
     {
         uint n_state = mp_decode_uint(&ip);