Skip to content

Commit

Permalink
[v1.1] Optimize a simple ':', 'true', 'false', 'break', 'continue'
Browse files Browse the repository at this point in the history
The following idioms are commonly used to create infinite loops:

	while :; do ...
	while true; do ...
	until false; do ...

In some scripts, that ':', 'true' or 'false' command may be run
millions of time as the loop body does some sort of calculation
until a condition is met that triggers a break from the loop.

Currently, those dummy commands have to go through the whole
sh_exec() rigmarole including sigsetjmp, etc. only to call a
builtin that does nothing but return a status value of 0 or 1.

If the dummy command does not contain arguments with expansions
(which may have side effects), assignments, redirections, and is
not being traced, then there is an opportunity for optimisation.

This commit adds a three-pronged optimisation approach, hopefully
without introducing regressions; please test.

src/cmd/ksh93/sh/xec.c: sh_exec():
- Refactor initialisation to make the first optimisation possible.

- First optimisation: At the start, optimize a simple literal ':',
  'true', 'false', 'break', or 'continue' if possible:
  * type==TCOM, this means a simple command with only literal
    arguments, if any (no expansions as the COMSCAN bit is not set)
  * For 'break' and 'continue': no arguments given at all
  * No variable assignments list
  * No I/O redirections
  * xtrace option not active
  * DEBUG trap not active
  * For 'false': errexit option and ERR trap not active
  For 'false' and 'break/continue', execute very fast simplified
  versions. For 'true' and ':', there is simply nothing to do.

- Second optimisation: case TCOM: In case the previous optimisation
  could not be applied, add a fallback optimisaton for ':', 'true'
  and 'false' (e.g., in case there were arguments containing
  expansions). This doesn't gain as much, but it's still worth
  avoiding the sigsetjmp which is relatively expensive. This can be
  done if there are no redirections or arguments.

- Third optimisation: case TWH: For a simple 'while :', 'while
  true' or 'until false', skip doing the recursive sh_exec() call.
  Set an always_true flag if the condition is always going to be
  true (and no expansions, assignments, redirections, xtrace, or
  DEBUG trap exist).

- case TFUN: Set the sh.dont_optimize_builtins flag (disabling all
  the optimizations above) if the user sets a shell function called
  'true', 'false', 'break' or 'continue'. This approach might be
  a bit crude, but the use case is rare, and the optimisation is
  nonessential, so I think it's not worth refining further.

On my system and default compiler flags (-Os), the following loop:
    typeset -i i; time while true; do ((++i<10000000))||break; done
takes 1.0 sec to run before this optimization and 0.6 secs after.
  • Loading branch information
McDutchie committed Feb 23, 2024
1 parent 8cabf4a commit 2e1e47d
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 28 deletions.
6 changes: 6 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ This documents significant changes in the dev branch of ksh 93u+m.
For full details, see the git log at: https://github.com/ksh93/ksh
Uppercase BUG_* IDs are shell bug IDs as used by the Modernish shell library.

2024-02-23:

- [v1.1] The commands ':', 'true', 'false', 'break' and 'continue' have been
optimised to run faster in loops in certain common cases. For example, the
'true' in 'while true; do some_stuff || break; done' now runs faster.

2024-02-22:

- Fixed a crash that occurred when starting ksh with no TERM variable in the
Expand Down
1 change: 1 addition & 0 deletions src/cmd/ksh93/include/shell.h
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ struct Shell_s
char *mathnodes;
char *bltin_dir;
char tilde_block; /* set to block .sh.tilde.{get,set} discipline */
char dont_optimize_builtins;
/* nv_putsub() hack for nv_create() to avoid double arithmetic evaluation */
char nv_putsub_already_called_sh_arith;
int nv_putsub_idx; /* saves array index obtained by nv_putsub() using sh_arith() */
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/ksh93/include/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

#define SH_RELEASE_FORK "93u+m" /* only change if you develop a new ksh93 fork */
#define SH_RELEASE_SVER "1.1.0-alpha" /* semantic version number: https://semver.org */
#define SH_RELEASE_DATE "2024-02-22" /* must be in this format for $((.sh.version)) */
#define SH_RELEASE_DATE "2024-02-23" /* must be in this format for $((.sh.version)) */
#define SH_RELEASE_CPYR "(c) 2020-2024 Contributors to ksh " SH_RELEASE_FORK

/* Scripts sometimes field-split ${.sh.version}, so don't change amount of whitespace. */
Expand Down
1 change: 1 addition & 0 deletions src/cmd/ksh93/sh/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@ static void exfile(Sfio_t *iop,int fno)
{
execflags |= sh_state(SH_NOFORK);
}
sh.dont_optimize_builtins = 0;
sh.st.breakcnt = 0;
sh_exec(t,execflags);
if(sh.forked)
Expand Down
107 changes: 80 additions & 27 deletions src/cmd/ksh93/sh/xec.c
Original file line number Diff line number Diff line change
Expand Up @@ -865,15 +865,59 @@ static int check_exec_optimization(int type, int execflg, int execflg2, struct i
*/
int sh_exec(const Shnode_t *t, int flags)
{
int type;
int mainloop;
sh_sigcheck();
if(t && sh.st.breakcnt==0 && !sh_isoption(SH_NOEXEC))
/* Bail out on no command, break/continue, or noexec */
if(!t || sh.st.breakcnt || sh_isoption(SH_NOEXEC))
return sh.exitval;
/* Set up state */
sh.exitval = 0;
sh.lastsig = 0;
sh.chldexitsig = 0;
type = t->tre.tretyp;
mainloop = (flags&sh_state(SH_INTERACTIVE));
if(mainloop)
{
if(pipejob==2)
job_unlock();
nlock = 0;
pipejob = 0;
job.curpgid = 0;
job.curjobid = 0;
flags &= ~sh_state(SH_INTERACTIVE);
}
/* Optimize a simple literal ':', 'true', 'false', 'break', 'continue' if the conditions are right */
else if(type==TCOM && !sh.dont_optimize_builtins) /* no COMSCAN bit in 'type' => no expansions */
{
Namval_t *np;
Shbltin_f fp;
if((np = (Namval_t*)t->com.comnamp) && (fp = funptr(np))
&& (fp==b_true || (fp==b_false && !sh_isoption(SH_ERREXIT) && !sh.st.trap[SH_ERRTRAP])
|| fp==b_break && t->com.comarg->argchn.len==1) /* for break/continue: 1 arg (command name) */
&& !t->com.comset /* no variable assignments list */
&& !t->com.comio /* no I/O redirections */
&& !sh_isoption(SH_XTRACE)
&& !sh.st.trap[SH_DEBUGTRAP])
{
/* Execute optimized basic versions of the builtins */
if(fp==b_false)
++sh.exitval;
else if(fp==b_break && sh.st.loopcnt)
sh.st.breakcnt = np==SYSCONT ? -1 : 1;
/* Set $?, possibly execute traps, and we're out of here */
exitset();
if(sh.trapnote)
sh_chktrap();
return sh.exitval;
}
}
/* Normal command execution */
{
int type = t->tre.tretyp;
char *com0 = 0;
int errorflg = (flags&sh_state(SH_ERREXIT))|(flags & ARG_OPTIMIZE);
int execflg = (flags&sh_state(SH_NOFORK));
int execflg2 = (flags&sh_state(SH_FORKED));
int mainloop = (flags&sh_state(SH_INTERACTIVE));
int topfd = sh.topfd;
char *sav=stkfreeze(sh.stk,0);
char *cp=0, **com=0, *comn;
Expand All @@ -883,22 +927,9 @@ int sh_exec(const Shnode_t *t, int flags)
volatile int was_errexit = sh_isstate(SH_ERREXIT);
volatile int was_monitor = sh_isstate(SH_MONITOR);
volatile int echeck = 0;
if(flags&sh_state(SH_INTERACTIVE))
{
if(pipejob==2)
job_unlock();
nlock = 0;
pipejob = 0;
job.curpgid = 0;
job.curjobid = 0;
flags &= ~sh_state(SH_INTERACTIVE);
}
sh_offstate(SH_DEFPATH);
if(!(flags & sh_state(SH_ERREXIT)))
sh_offstate(SH_ERREXIT);
sh.exitval=0;
sh.lastsig = 0;
sh.chldexitsig = 0;
switch(type&COMMSK)
{
/*
Expand Down Expand Up @@ -1157,17 +1188,23 @@ int sh_exec(const Shnode_t *t, int flags)
/* check for builtins */
if(np && is_abuiltin(np))
{
volatile char scope=0, share=0, was_mktype=(sh.mktype!=NULL);
volatile void *save_ptr;
volatile void *save_data;
int save_prompt;
int was_nofork = execflg?sh_isstate(SH_NOFORK):0;
struct checkpt *buffp = (struct checkpt*)stkalloc(sh.stk,sizeof(struct checkpt));
volatile char scope, share, was_mktype, was_nofork;
volatile void *save_ptr;
volatile void *save_data;
int save_prompt;
struct checkpt *buffp;
/* Fallback optimization for ':'/'true' and 'false' */
if(!io && !argp && (funptr(np)==b_true || funptr(np)==b_false && ++sh.exitval))
goto setexit;
scope = 0, share = 0;
was_mktype = sh.mktype!=NULL;
was_nofork = execflg && sh_isstate(SH_NOFORK);
bp = &sh.bltindata;
save_ptr = bp->ptr;
save_data = bp->data;
if(execflg)
sh_onstate(SH_NOFORK);
buffp = (struct checkpt*)stkalloc(sh.stk,sizeof(struct checkpt));
sh_pushcontext(buffp,SH_JMPCMD);
jmpval = sigsetjmp(buffp->buff,0);
if(jmpval == 0)
Expand Down Expand Up @@ -2080,6 +2117,9 @@ int sh_exec(const Shnode_t *t, int flags)
volatile int r=0;
int first = ARG_OPTIMIZE;
Shnode_t *tt = t->wh.whtre;
char always_true;
Namval_t *np;
Shbltin_f fp;
#if SHOPT_FILESCAN
Sfio_t *iop=0;
int savein=-1;
Expand Down Expand Up @@ -2110,6 +2150,15 @@ int sh_exec(const Shnode_t *t, int flags)
iop = openstream(tt->com.comio,&savein);
}
#endif /* SHOPT_FILESCAN */
/* Optimization: don't call sh_exec() for simple 'while :', 'while true' or 'until false' */
always_true = (tt->tre.tretyp==TCOM /* one simple command (no COMSCAN = no expansions) */
&& !sh.dont_optimize_builtins
&& (np = (Namval_t*)tt->com.comnamp) && (fp = funptr(np))
&& (type==TWH && fp==b_true || type==TUN && fp==b_false)
&& !tt->com.comset /* no variable assignments list */
&& !tt->com.comio /* no I/O redirections */
&& !sh_isoption(SH_XTRACE)
&& !sh.st.trap[SH_DEBUGTRAP]);
sh.st.loopcnt++;
while(sh.st.breakcnt==0)
{
Expand All @@ -2121,7 +2170,7 @@ int sh_exec(const Shnode_t *t, int flags)
}
else
#endif /* SHOPT_FILESCAN */
if((sh_exec(tt,first)==0)!=(type==TWH))
if(!always_true && (sh_exec(tt,first)==0)!=(type==TWH))
break;
r = sh_exec(t->wh.dotre,first|errorflg);
/* decrease 'continue' level */
Expand Down Expand Up @@ -2390,7 +2439,6 @@ int sh_exec(const Shnode_t *t, int flags)
#endif /* SHOPT_NAMESPACE */
/* look for discipline functions */
error_info.line = t->funct.functline-sh.st.firstline;
/* Function names cannot be special builtin */
if(cp || sh.prefix)
{
int offset = stktell(sh.stk);
Expand All @@ -2412,10 +2460,15 @@ int sh_exec(const Shnode_t *t, int flags)
sfprintf(sh.stk,"%s.%s%c",nv_name(npv),cp,0);
fname = stkptr(sh.stk,offset);
}
else if((mp=nv_search(fname,sh.bltin_tree,0)) && nv_isattr(mp,BLT_SPC))
else if((mp=nv_search(fname,sh.bltin_tree,0)))
{
errormsg(SH_DICT,ERROR_exit(1),e_badfun,fname);
UNREACHABLE();
if(nv_isattr(mp,BLT_SPC))
{ /* Function names cannot be special builtin */
errormsg(SH_DICT,ERROR_exit(1),e_badfun,fname);
UNREACHABLE();
}
if(funptr(mp)==b_true || funptr(mp)==b_false || funptr(mp)==b_break)
sh.dont_optimize_builtins = 1;
}
#if SHOPT_NAMESPACE
if(sh.namespace && !sh.prefix && *fname!='.')
Expand Down

0 comments on commit 2e1e47d

Please sign in to comment.