I have noticed that executing subshells makes a program very slow. For example, here code 1 runs way slower than code 2:
1.
#!/usr/bin/env ruby
$-v = true
require 'io/console'
unless STDOUT.tty?
res = [40, 40].freeze
IO.tap { |x| x.undef_method(:winsize) }.tap { |x| x.define_method(:winsize) { res } }
end
i = 0
while true
i += 1
h, w = STDOUT.winsize
print `clear`, ?\n.freeze.*(h / 2), Time.new.strftime("%H:%M:%S:%2N::#{i}").center(w)
end
2.
#!/usr/bin/env ruby
$-v = true
require 'io/console'
unless STDOUT.tty?
res = [40, 40].freeze
IO.tap { |x| x.undef_method(:winsize) }.tap { |x| x.define_method(:winsize) { res } }
end
i = 0
while true
i += 1
h, w = STDOUT.winsize
print "\e[2J\e[H\e[3J", ?\n.freeze.*(h / 2), Time.new.strftime("%H:%M:%S:%2N::#{i}").center(w)
end
Preview
Explanation
The program shows the current time, and the ::n shows the iteration count.
What I wanted to say is that executing commands in a subshell makes it a lot slower than doing that same thing in Ruby. Here clear
can be replaced with \e[2J\eH\e[3J
which works 100 times faster.
Why is executing commands in the subshell way slower?
I have noticed this a year ago when my friend was telling me to use clear instead of fancy \e[2J\e[H\e[3J
ANSI escape sequence. I told him it’s always slower to execute clear
because:
-
Ruby has to create a new shell and then execute the command.
-
The shell even has to find the binary from the exported PATH every time it re-executes in the loop.
-
Also, the Linux Kernel has to create a brand new process table for that process, which also increases the process count like so. Each time the loop executes clear
, a new process is created.
Here’s a program (p.rb) to demonstrate that new process is created with every single iteration by 1.rb. The program p.rb just looks at the /proc/ directory. Take a look at this animation. The 1.rb and 2.rb are are same as in the question.
Here 1.rb is creating new processes while 2.rb doesn’t.
Here’s the code of p.rb:
#!/usr/bin/env ruby
$-v = true
max, anims = 0, %w(| / - \\)
while true
pids = Dir['/proc/[0-9]*'.freeze].map! { |x| x.split(?/)[-1].to_i }
max = pids.max if max < pids.max
print "\e[1;33m #{anims.rotate![0]} #{max}\e[0m\r"
sleep 0.05
end
Point 3 will happen for things like shell’s cat
vs. IO.read
like in PTY.spawn('cat /proc/self/comm')[2]
vs. IO.read('/proc/self/comm')
where IO.read(...)
will be faster and won’t create new processes. The same will happen if you do something like Open3.popen3('/usr/bin/echo something')
.
On the other hand point 3 makes it harder to maintain a server running such server because the process count will be in millions!
So it’s recommended to stick with what ruby offers!
These are some reasons that came to my mind. I appreciate replies!