TL;DR: there is no performance gain in changing the chained attr methods for a single function that sets all attributes at once.
We can agree that a typical D3 code is quite repetitive, sometimes with a dozen attr methods chained. As a D3 programmer I'm used to it now, but I understand the fact that a lot of programmers cite that as their main complaint regarding D3.
In this answer I'll not discuss if that is good or bad, ugly or beautiful, nice or unpleasant. That would be just an opinion, and a worthless one. In this answer I'll focus on performance only.
First, let's consider a few hypothetical solutions:
Using d3-selection-multi: that may seem as the perfect solution, but actually it changes nothing: in its source code, d3-selection-multi simply gets the passed object and call selection.attr several times, just like your first snippet.
However, if performance (your #1) is not an issue and your only concern is readability and testability (as in your #2), I'd go with d3-selection-multi.
Using selection.each: I believe that most D3 programmers will immediately think about encapsulating the chained attr in an each method. But in fact this changes nothing:
selection.each((d, i, n)=>{
d3.select(n[i])
.attr("foo", foo)
.attr("bar", bar)
//etc...
});
As you can see, the chained attr are still there. It's even worse, not that we have an additional each (attr uses selection.each internally)
Using selection.call or any other alternative and passing the same chained attr methods to the selection.
These are not adequate alternatives when it comes to performance. So, let's try another ways of improving performance.
Examining the source code of attr we can see that, internally, it uses Element.setAttribute or Element.setAttributeNS. With that information, let's try to recreate your pseudocode with a method that loops the selection only once. For that, we'll use selection.each, like this:
selection.each((d, i, n) => {
n[i].setAttribute("cx", d.x);
n[i].setAttribute("cy", d.y);
n[i].setAttribute("r", 2);
})
Finally, let's test it. For this benchmark I wrote a very simple code, setting the cx, cy and r attributes of some circles. This is the default approach:
const data = d3.range(100).map(() => ({
x: Math.random() * 300,
y: Math.random() * 150
}));
const svg = d3.select("body")
.append("svg");
const circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cx", d=>d.x)
.attr("cy", d=>d.y)
.attr("r", 2)
.style("fill", "teal");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
And this the approach using setAttribute in a single loop:
const data = d3.range(100).map(() => ({
x: Math.random() * 300,
y: Math.random() * 150
}));
const svg = d3.select("body")
.append("svg");
const circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle")
.each((d, i, n) => {
n[i].setAttribute("cx", d.x);
n[i].setAttribute("cy", d.y);
n[i].setAttribute("r", 2);
})
.style("fill", "teal")
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Finally, the most important moment: let's benchmark it. I normally use jsPerf, but it's down for me, so I'm using another online tool. Here it is:
https://measurethat.net/Benchmarks/Show/6750/0/multiple-attributes
And the results were disappointing, there is virtually no difference:

There is some fluctuation, sometimes one code is faster, but most of the times they are pretty equivalent.
However, it gets even worse: as another user correctly pointed in their comment, the correct and dynamic approach would involve looping again in your second pseudocode. That would make the performance even worse:

Therefore, the problem is that your claim ("No matter how fast the iteration control, it's still faster if we iterate once rather than three times") doesn't need to be necessarily true. Think like that: if you had a selection of 15 elements and 4 attributes, the question would be "is it faster doing 15 external loops with 4 internal loops each or doing 4 external loops with 15 internal loops each?". As you can see, nothing allows us to say that one is faster than the other.
Conclusion: there is no performance gain in changing the chained attr methods for a single function that sets all attributes at once.
attrs, but we have a set of distinct manipulations within each fat datum. Within that set, we're not even iterating. It's simply a sequence of operations that we need to do/update.