Doing same strace, you can see the differences:
With pipe:
$ strace -c ./pipe.sh % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 57.89 0.103005 5 20000 clone 40.81 0.072616 2 30000 10000 wait4 0.58 0.001037 0 120008 rt_sigprocmask 0.40 0.000711 0 10000 pipe
With proc-sub:
$ strace -c ./procsub.sh % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 85.08 0.045502 5 10000 clone 3.25 0.001736 0 90329 322 read 2.12 0.001133 0 20009 open 2.03 0.001086 0 50001 dup2
With above statistics, you can see pipe create more child processes and spending many times to wait child process to finish for parent process to continue executing.
Process substitution is not. It can read directly from child processes. Process substitution is performed at the same time with parameter and variable expansion, the command in Process Substitution run in background. From bash manpage:
Process Substitution Process substitution is supported on systems that support named pipes (FIFOs) or the /dev/fd method of naming open files. It takes the form of <(list) or >(list). The process list is run with its input or out‐ put connected to a FIFO or some file in /dev/fd. The name of this file is passed as an argument to the current command as the result of the expansion. If the >(list) form is used, writing to the file will pro‐ vide input for list. If the <(list) form is used, the file passed as an argument should be read to obtain the output of list. When available, process substitution is performed simultaneously with parameter and variable expansion, command substitution, and arithmetic expansion.