Showing posts with label linux. Show all posts
Showing posts with label linux. Show all posts

2026/04/06

Maximizing sshfs throughput in high-speed networks

It takes slightly more than 125 milliseconds to transfer 16MiB of data through a TCP connection over a 1Gib/s link.

The Linux default 128KiB socket receive buffer gets full in ~0.976 milliseconds when receiving from a 1Gib/s link. A receiving process must read the 128KiB socket receive buffer dry at a minimum rate of 1024Hz, in order to be able to receive at the link's 1Gib/s rate at all. Such rate is possible but not sustainable with default distribution kernels and configurations.

CODEL buffer-bloat prevention, aggravated by 128KiB-small default socket buffer sizes (hardly adequate for 1Gib/second links), minimises the network transfer latency at the expense of choking throughput with smaller network adaptor buffers. CODEL is only meaningful when hosts are capable of fully saturating/congesting a network link and it is desirable to minimise latency for the price of lower throughput.

Using sshfs in high-speed local networks demands maximum throughput, rather than low latency — the extreme opposite of CODEL network latency minimisation achieved by making network adaptor buffers tiny.

Tiny buffers minimise Ethernet frame queue sizes and, hence, queuing delays, which throttles senders in connections with flow-control, such as TCP. TCP peers explicitly communicate available receive buffer capacities, so that a TCP sender pauses sending any further TCP segments until the receiver advertises a non-zero receive buffer size.

UDP has no flow-control, so that protocols communicating with UDP datagrams, such as WireGuard, end up hammering the CPU with non-blocking write/send/sendto syscall retries when a tiny network adaptor send buffer gets full and rejects queuing the next Ethernet frame.

ssh compression throughput is limited to ~200MiB/s, when compressing with a CPU core running at 3.5GHz (top cloud CPU cores run at 2.8GHz).

With ssh compression disabled, ssh encryption is the next data transfer bottleneck. No ssh cipher is capable of encrypting/decrypting at or above 1Gib/s rate, apart from aes128-gcm@openssh.com running on CPUs with the AESNI instruction set.

With above facts in mind, maximising throughput for sshfs mounts over 1Gib/s or faster network links requires invoking sshfs with additional option -o compression=no,Ciphers=aes128-gcm@openssh.com to remove ssh data transfer bottlenecks, when AESNI instruction set is available on both peers of an ssh connection.

E.g. before:

sshfs -o reconnect,idmap=user,noatime ...

After:

sshfs -o reconnect,idmap=user,noatime -o compression=no,Ciphers=aes128-gcm@openssh.com ...

Next, maximising the throughput of 1Gib/s or faster network links requires enabling much larger socket buffer sizes, and using much larger socket buffers by default.

Using a much larger sshfs data transfer size per request (than the default 256 FUSE pages, controlled by fs.fuse.max_pages_limit) significantly improves sshfs performance over high-bandwidth network connections too.

These parameters have to be configured with sysctl. E.g.:

# /etc/sysctl.d/98-maximize-net-throughput.conf
net.core.rmem_default = 16777216
net.core.wmem_default = 16777216
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
fs.fuse.max_pages_limit = 32768

Furthermore, enabling Jumbo Ethernet frames reduces the cost/waste of Ethernet frame headers and trailers in large data transfers. Jumbo Ethernet ~9KiB frames improve data transfer speeds by 5-10%, when/if all hosts involved in the particilar route support jumbo Ethernet frame sizes. E.g. both peers in the same network and the switch/router the peers are connected to.

Jumbo Ethernet frames are enabled by explicitly setting connection/link MTU to the maximum MTU supported by particular network adaptors (adaptor's maxmtu in ip -d link show output), instead of default "auto" (1500-byte) MTU in network connection settings.

Wireless adaptors also support Jumbo Frames, contrary to all claims elsewhere. A laptop I type this text on, for example, supports 9216-byte jumbo frames for wired Ethernet and 2304-byte jumbo frames over WiFi.

2009/11/14

Using Linux ps utility to display the command line of a process

Sometimes it is useful to see the full command line of a process when using Linux ps utility. Linux ps utility obeys COLUMNS environment variable or --columns command line option to limit the line size of the output. The default values are normally too small and cause ps to truncate the command line:
[max@truth ~]$ ps -u $USER -fH
UID PID PPID C STIME TTY TIME CMD
max 7421 7385 0 17:04 ? 00:00:00 gnome-session
max 7527 7421 0 17:04 ? 00:00:02 metacity
max 7539 7421 0 17:04 ? 00:00:00 gnome-panel
max 7540 7421 0 17:04 ? 00:00:00 nautilus
max 7541 7421 0 17:04 ? 00:00:00 /usr/libexec/gdu-notification-
max 7543 7421 0 17:04 ? 00:00:00 /usr/bin/seapplet
max 7544 7421 0 17:04 ? 00:00:00 gnome-power-manager
max 7545 7421 0 17:04 ? 00:00:00 gpk-update-icon
max 7550 7421 0 17:04 ? 00:00:00 bluetooth-applet
max 7551 7421 0 17:04 ? 00:00:00 gnome-volume-control-applet
max 7552 7421 0 17:04 ? 00:00:00 nm-applet --sm-disable
max 7559 7421 0 17:04 ? 00:00:00 kerneloops-applet
max 7561 7421 0 17:04 ? 00:00:00 python /usr/share/system-confi
max 8127 1 0 17:25 ? 00:00:01 gcalctool
max 7942 1 0 17:12 ? 00:00:00 /bin/sh /usr/lib64/firefox-3.5.4
max 7954 7942 2 17:12 ? 00:01:34 /usr/lib64/firefox-3.5.4/firef
max 7774 1 0 17:06 ? 00:00:09 emacs
max 7821 7774 0 17:06 pts/0 00:00:00 /bin/bash --noediting -i
max 8600 7821 0 18:15 pts/0 00:00:00 ps -u max -fH
max 7740 1 0 17:05 ? 00:00:00 gnome-screensaver
max 7730 1 2 17:05 ? 00:01:30 rhythmbox
max 7728 1 0 17:05 ? 00:00:00 /usr/libexec/gvfsd-burn --spawne
max 7719 1 0 17:05 ? 00:00:00 /usr/libexec/notification-area-a
max 7717 1 0 17:05 ? 00:00:02 /usr/libexec/clock-applet --oaf-
max 7715 1 0 17:05 ? 00:00:08 /usr/libexec/multiload-applet-2
max 7711 1 0 17:05 ? 00:00:00 /usr/libexec/cpufreq-applet --oa
max 7708 1 0 17:05 ? 00:00:00 /usr/libexec/wnck-applet --oaf-a
max 7698 1 0 17:04 ? 00:00:00 /usr/libexec/gvfs-gphoto2-volume
max 7693 1 0 17:04 ? 00:00:00 /usr/libexec/gvfs-gdu-volume-mon
max 7691 1 0 17:04 ? 00:00:00 /usr/libexec/bonobo-activation-s
max 7687 1 0 17:04 ? 00:00:00 /usr/libexec/gvfsd-trash --spawn
max 7685 1 0 17:04 ? 00:00:00 /usr/libexec/gconf-im-settings-d
max 7572 1 0 17:04 ? 00:00:00 /usr/libexec/im-settings-daemon
max 7569 1 0 17:04 ? 00:00:00 /usr/libexec/notification-daemon
max 7535 1 0 17:04 ? 00:00:00 /usr/libexec//gvfs-fuse-daemon /
max 7529 1 0 17:04 ? 00:00:00 /usr/libexec/gvfsd
max 7519 1 0 17:04 ? 00:00:00 /usr/libexec/gnome-settings-daem
max 7516 1 0 17:04 ? 00:00:00 /usr/libexec/gconfd-2
max 7433 1 0 17:04 ? 00:00:00 dbus-launch --sh-syntax --exit-w
max 7432 1 0 17:04 ? 00:00:00 /bin/dbus-daemon --fork --print-
max 7405 1 0 17:04 ? 00:00:00 /usr/bin/gnome-keyring-daemon --
max 7320 1 1 17:04 ? 00:01:07 /usr/bin/pulseaudio --start --lo
max 7323 7320 0 17:04 ? 00:00:00 /usr/libexec/pulse/gconf-helpe
The maximum command line length of a process can be read from ARG_MAX sysconf variable
[max@truth ~]$ getconf ARG_MAX
2621440
On my system it is 2.5Mb. The first idea that comes to mind to make ps show the full command line is to pass ARG_MAX (+ some constant for other ps output fields) as the line limit to ps.
[max@truth ~]$ ps -u $USER -fH --columns=`getconf ARG_MAX`
Fix bigness error.
UID PID PPID C STIME TTY TIME CMD
Fix bigness error.
max 7421 7385 0 17:04 ? 00:00:00 gnome-session
Fix bigness error.
max 7527 7421 0 17:04 ? 00:00:02 metacity
Fix bigness error.
...
However, ps does not seem to like the columns value. A quick binary search for the maximum of --columns value reveals:
[max@truth ~]$ ps -u $USER -fH --columns=131073
Fix bigness error.
UID PID PPID C STIME TTY TIME CMD
Fix bigness error.
max 7421 7385 0 17:04 ? 00:00:00 gnome-session
Fix bigness error.
max 7527 7421 0 17:04 ? 00:00:02 metacity
Fix bigness error.
...

[max@truth ~]$ ps -u $USER -fH --columns=131072
UID PID PPID C STIME TTY TIME CMD
max 7421 7385 0 17:04 ? 00:00:00 gnome-session
max 7527 7421 0 17:04 ? 00:00:02 metacity
max 7539 7421 0 17:04 ? 00:00:00 gnome-panel
max 7540 7421 0 17:04 ? 00:00:00 nautilus
max 7541 7421 0 17:04 ? 00:00:00 /usr/libexec/gdu-notification-daemon
max 7543 7421 0 17:04 ? 00:00:00 /usr/bin/seapplet
max 7544 7421 0 17:04 ? 00:00:00 gnome-power-manager
max 7545 7421 0 17:04 ? 00:00:00 gpk-update-icon
max 7550 7421 0 17:04 ? 00:00:00 bluetooth-applet
max 7551 7421 0 17:04 ? 00:00:00 gnome-volume-control-applet
max 7552 7421 0 17:04 ? 00:00:00 nm-applet --sm-disable
max 7559 7421 0 17:04 ? 00:00:00 kerneloops-applet
max 7561 7421 0 17:04 ? 00:00:00 python /usr/share/system-config-printer/applet.py
max 8127 1 0 17:25 ? 00:00:01 gcalctool
max 7942 1 0 17:12 ? 00:00:00 /bin/sh /usr/lib64/firefox-3.5.4/run-mozilla.sh /usr/lib64/firefox-3.5.4/firefox
max 7954 7942 2 17:12 ? 00:01:36 /usr/lib64/firefox-3.5.4/firefox
max 7774 1 0 17:06 ? 00:00:10 emacs
max 7821 7774 0 17:06 pts/0 00:00:00 /bin/bash --noediting -i
max 8629 7821 0 18:21 pts/0 00:00:00 ps -u max -fH --columns=131072
max 7740 1 0 17:05 ? 00:00:00 gnome-screensaver
max 7730 1 2 17:05 ? 00:01:37 rhythmbox
max 7728 1 0 17:05 ? 00:00:00 /usr/libexec/gvfsd-burn --spawner :1.7 /org/gtk/gvfs/exec_spaw/1
max 7719 1 0 17:05 ? 00:00:00 /usr/libexec/notification-area-applet --oaf-activate-iid=OAFIID:GNOME_NotificationAreaApplet_Factory --oaf-ior-fd=27
max 7717 1 0 17:05 ? 00:00:02 /usr/libexec/clock-applet --oaf-activate-iid=OAFIID:GNOME_ClockApplet_Factory --oaf-ior-fd=30
max 7715 1 0 17:05 ? 00:00:09 /usr/libexec/multiload-applet-2 --oaf-activate-iid=OAFIID:GNOME_MultiLoadApplet_Factory --oaf-ior-fd=21
max 7711 1 0 17:05 ? 00:00:00 /usr/libexec/cpufreq-applet --oaf-activate-iid=OAFIID:GNOME_CPUFreqApplet_Factory --oaf-ior-fd=24
max 7708 1 0 17:05 ? 00:00:00 /usr/libexec/wnck-applet --oaf-activate-iid=OAFIID:GNOME_Wncklet_Factory --oaf-ior-fd=18
max 7698 1 0 17:04 ? 00:00:00 /usr/libexec/gvfs-gphoto2-volume-monitor
max 7693 1 0 17:04 ? 00:00:00 /usr/libexec/gvfs-gdu-volume-monitor
max 7691 1 0 17:04 ? 00:00:00 /usr/libexec/bonobo-activation-server --ac-activate --ior-output-fd=19
max 7687 1 0 17:04 ? 00:00:00 /usr/libexec/gvfsd-trash --spawner :1.7 /org/gtk/gvfs/exec_spaw/0
max 7685 1 0 17:04 ? 00:00:00 /usr/libexec/gconf-im-settings-daemon
max 7572 1 0 17:04 ? 00:00:00 /usr/libexec/im-settings-daemon
max 7569 1 0 17:04 ? 00:00:00 /usr/libexec/notification-daemon
max 7535 1 0 17:04 ? 00:00:00 /usr/libexec//gvfs-fuse-daemon /home/max/.gvfs
max 7529 1 0 17:04 ? 00:00:00 /usr/libexec/gvfsd
max 7519 1 0 17:04 ? 00:00:00 /usr/libexec/gnome-settings-daemon
max 7516 1 0 17:04 ? 00:00:00 /usr/libexec/gconfd-2
max 7433 1 0 17:04 ? 00:00:00 dbus-launch --sh-syntax --exit-with-session
max 7432 1 0 17:04 ? 00:00:00 /bin/dbus-daemon --fork --print-pid 9 --print-address 11 --session
max 7405 1 0 17:04 ? 00:00:00 /usr/bin/gnome-keyring-daemon --daemonize --login
max 7320 1 1 17:04 ? 00:01:13 /usr/bin/pulseaudio --start --log-target=syslog
max 7323 7320 0 17:04 ? 00:00:00 /usr/libexec/pulse/gconf-helper
It looks like there is a hardcoded maximum of the line length in Linux Fedora 11 ps utility equal to 128Kb (0x20000 in hex). It would be nice to remove that limit.

p.s.: The lines of this post get truncated when viewed as a web-page using Firefox or Chromium. To view untruncated use an RSS reader, such as Google Reader.

p.p.s.: And yes, I should have updated my Firefox to 3.5.5 which is available on Fedora 11 updates repository now.

2009/11/12

Benchmarking function call overhead in C++


I've caught some cold and been staying at home. Just for fun, decided to compare the price of calls to:
  • a regular function from the same non-position-independent-code (non-PIC) executable object
  • a regular function from a PIC shared library object
  • a virtual function from the same non-PIC executable object
  • a virtual function from a PIC shared library object
Here is the source code of the benchmark:
[max@truth test]$ cat a.h
#pragma once

#include <memory>

namespace max {

void funA(long*); // same non-pic executable, different .o
void funB(long*); // another pic shared library

struct X
{
virtual ~X() {}
virtual void fun(long*) = 0;
};

std::auto_ptr<X> createA(); // same non-pic executable, different .o
std::auto_ptr<X> createB(); // another pic shared library

}

[max@truth test]$ cat a.cc
#include "a.h"

void max::funA(long* x) { ++*x; }

namespace {

struct ImpOfX : max::X
{
void fun(long* x) { ++*x; }
};

}

std::auto_ptr<max::X> max::createA()
{
return std::auto_ptr<max::X>(new ImpOfX);
}

[max@truth test]$ cat b.cc
#include "a.h"

void max::funB(long* x) { ++*x; }

namespace {

struct ImpOfX2 : max::X
{
void fun(long* x) { ++*x; }
};

}

std::auto_ptr<max::X> max::createB()
{
return std::auto_ptr<max::X>(new ImpOfX2);
}

[max@truth test]$ cat main.cc
#include "a.h"
#include <stdio.h>
#include <time.h>

using namespace max;

namespace {

typedef unsigned long long nsec_t;

nsec_t now()
{
timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * nsec_t(1000000000) + ts.tv_nsec;
}

long benchmark(long n, X& a, X& b, double times[4])
{
long sum[4] = {};

nsec_t start, stop;
start = now();
for(int i = n; i--;) {
funA(sum + 0);
}
stop = now();
times[0] = stop - start;
start = stop;

for(int i = n; i--;) {
funB(sum + 1);
}
stop = now();
times[1] = stop - start;
start = stop;

for(int i = n; i--;) {
a.fun(sum + 2);
}
stop = now();
times[2] = stop - start;
start = stop;

for(int i = n; i--;) {
b.fun(sum + 3);
}
stop = now();
times[3] = stop - start;
start = stop;

return sum[0] + sum[1] + sum[2] + sum[3];
}

}

int main()
{
std::auto_ptr<X> a(createA());
std::auto_ptr<X> b(createB());

double times[4];
// warm-up CPU caches and branch predictors
long sum = benchmark(100, *a, *b, times);
// benchmark
long const N = 1e9;
sum += benchmark(N, *a, *b, times);

printf(
"%lu calls to: \n"
" a regular function from the same executable: %.3lfnsec/call\n"
" a regular function from a shared library: %.3lfnsec/call\n"
" a virtual function from the same executable: %.3lfnsec/call\n"
" a virtual function from a shared library: %.3lfnsec/call\n"
, N
, times[0] / N
, times[1] / N
, times[2] / N
, times[3] / N
);
}
To get nanosecond-resolution times clock_gettime() function is used.

This is how it is compiled:
[max@truth test]$ g++ -m64 -Wall -Wextra -march=native -O3 -c main.cc
[max@truth test]$ g++ -m64 -Wall -Wextra -march=native -O3 -c a.cc
[max@truth test]$ g++ -m64 -Wall -Wextra -march=native -O3 -fpic -c b.cc
[max@truth test]$ g++ -m64 -Wl,-rpath,'$ORIGIN' -Wl,-z,now -shared -o libb.so b.o
[max@truth test]$ g++ -m64 -Wl,-rpath,'$ORIGIN' -Wl,-z,now -o test main.o a.o libb.so -lrt
When linking -Wl,-z,now linker option disables lazy symbol binding. Lazy symbol binding resolves calls to functions from PIC objects on the first call. Disabling lazy binding makes the dynamic linker resolve all symbols prior to executing any application pre, rather than on the first access.

The other linker option -Wl,-rpath,'$ORIGIN' tells the dynamic linker to search for the shared libraries first in the same folder where the executable is, so that there is no need to fiddle with LD_LIBRARY_PATH variable or /etc/ld.so.conf.

And here are the results of the benchmark:
[max@truth test]$ sudo chrt -f 99 /usr/bin/time -f "%E elapsed, %c context switches" ./test
1000000000 calls to:
a regular function from the same executable: 1.999nsec/call
a regular function from a shared library: 2.665nsec/call
a virtual function from the same executable: 2.332nsec/call
a virtual function from a shared library: 2.332nsec/call
0:09.32 elapsed, 0 context switches
The test is invoked as a FIFO realtime class process with priority 99 (highest on my system). FIFO realtime class processes run until they voluntarily block themselves, time quantums don't apply.

In the above invocation GNU /usr/bin/time utility is used to report the number of context switches during the run to make sure that the process has executed uninterrupted. This is to eliminate scheduling noise from the timings completely.

The test was invoked on Intel E8400 CPU running at full 3GHz speed.

Interpretation of the results:
  • Calls to regular functions in non-PIC object files are the cheapest.
  • Calls to virtual member functions are more expensive. An interesting fact here is that calls to virtual functions always get dispatched through the virtual table. This means that the procedure linking tables (PLT) used for regular functions in PIC object files is bypassed entirely. There is no difference whether a virtual function resides in PIC or non-PIC object, they get called the same way through the virtual table.
  • Calls to regular functions in PIC objects are most expensive. This is because such calls in fact call code in the PLT which (when the function has been resolved) jumps to the actual function.
Hm..., interesting!